Skip to content

Commit

Permalink
Merge pull request #571 from hashicorp/feature/service-principal-feat…
Browse files Browse the repository at this point in the history
…ures

azuread_service_principal: support the features block
  • Loading branch information
manicminer authored Sep 14, 2021
2 parents 43b77e2 + bca4c00 commit e5644fc
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 15 deletions.
10 changes: 10 additions & 0 deletions docs/data-sources/service_principal.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ The following attributes are exported:
* `application_tenant_id` - The tenant ID where the associated application is registered.
* `description` - A description of the service principal provided for internal end-users.
* `display_name` - The display name of the application associated with this service principal.
* `features` - A `features` block as described below.
* `homepage_url` - Home page or landing page of the associated application.
* `login_url` - The URL where the service provider redirects the user to Azure AD to authenticate. Azure AD uses the URL to launch the application from Microsoft 365 or the Azure AD My Apps.
* `logout_url` - The URL that will be used by Microsoft's authorization service to logout an user using OpenId Connect front-channel, back-channel or SAML logout protocols, taken from the associated application.
Expand Down Expand Up @@ -93,6 +94,15 @@ The following attributes are exported:

---

`features` block exports the following:

* `custom_single_sign_on_app` - Whether this service principal represents a custom SAML application.
* `enterprise_application` - Whether this service principal represents an Enterprise Application.
* `gallery_application` - Whether this service principal represents a gallery application.
* `visible_to_users` - Whether this app is visible to users in My Apps and Office 365 Launcher.

---

`oauth2_permission_scopes` block exports the following:

* `admin_consent_description` - Delegated permission description that appears in all tenant-wide admin consent experiences, intended to be read by an administrator granting the permission on behalf of all users.
Expand Down
34 changes: 32 additions & 2 deletions docs/resources/service_principal.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,32 @@ resource "azuread_application" "example" {
owners = [data.azuread_client_config.current.object_id]
}
resource "azuread_service_principal" "example" {
application_id = azuread_application.example.application_id
app_role_assignment_required = false
owners = [data.azuread_client_config.current.object_id]
}
```

*Create a service principal for an enterprise application*

```terraform
data "azuread_client_config" "current" {}
resource "azuread_application" "example" {
display_name = "example"
owners = [data.azuread_client_config.current.object_id]
}
resource "azuread_service_principal" "example" {
application_id = azuread_application.example.application_id
app_role_assignment_required = false
owners = [data.azuread_client_config.current.object_id]
tags = ["example", "tags", "here"]
features {
enterprise_application = true
gallery_application = true
}
}
```

Expand Down Expand Up @@ -75,6 +95,7 @@ The following arguments are supported:
* `app_role_assignment_required` - (Optional) Whether this service principal requires an app role assignment to a user or group before Azure AD will issue a user or access token to the application. Defaults to `false`.
* `application_id` - (Required) The application ID (client ID) of the application for which to create a service principal.
* `description` - (Optional) A description of the service principal provided for internal end-users.
* `features` - (Optional) A `features` block as described below. Cannot be used together with the `tags` property.
* `login_url` - (Optional) The URL where the service provider redirects the user to Azure AD to authenticate. Azure AD uses the URL to launch the application from Microsoft 365 or the Azure AD My Apps. When blank, Azure AD performs IdP-initiated sign-on for applications configured with SAML-based single sign-on.
* `notes` - (Optional) A free text field to capture information about the service principal, typically used for operational purposes.
* `notification_email_addresses` - (Optional) A set of email addresses where Azure AD sends a notification when the active certificate is near the expiration date. This is only for the certificates used to sign the SAML token issued for Azure AD Gallery applications.
Expand All @@ -84,13 +105,22 @@ The following arguments are supported:

* `preferred_single_sign_on_mode` - (Optional) The single sign-on mode configured for this application. Azure AD uses the preferred single sign-on mode to launch the application from Microsoft 365 or the Azure AD My Apps. Supported values are `oidc`, `password`, `saml` or `notSupported`. Omit this property or specify a blank string to unset.
* `saml_single_sign_on` - (Optional) A `saml_single_sign_on` block as documented below.
* `tags` - (Optional) A set of tags to apply to the service principal.
* `tags` - (Optional) A set of tags to apply to the service principal. Cannot be used together with the `features` block.
* `use_existing` - (Optional) When true, any existing service principal linked to the same application will be automatically imported. When false, an import error will be raised for any pre-existing service principal.

-> **Caveats of `use_existing`** Enabling this behaviour is useful for managing existing service principals that may already be installed in your tenant for Microsoft-published APIs, as it allows you to make changes where permitted, and then also reference them in your Terraform configuration. However, the behaviour of delete operations is also affected - when `use_existing` is `true`, Terraform will still attempt to delete the service principal on destroy, although it will not raise an error if the deletion fails (as it often the case for first-party Microsoft applications).

---

`features` block supports the following:

* `custom_single_sign_on_app` - (Optional) Whether this service principal represents a custom SAML application. Defaults to `false`.
* `enterprise_application` - (Optional) Whether this service principal represents an Enterprise Application. Defaults to `false`.
* `gallery_application` - (Optional) Whether this service principal represents a gallery application. Defaults to `false`.
* `visible_to_users` - (Optional) Whether this app is visible to users in My Apps and Office 365 Launcher. Defaults to `true`.

---

`saml_single_sign_on` supports the following:

* `relay_state` - (Optional) The relative URI the service provider would redirect to after completion of the single sign-on flow.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,39 @@ func servicePrincipalData() *schema.Resource {
Computed: true,
},

"features": {
Description: "Block of features configured for this service principal using tags",
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"custom_single_sign_on_app": {
Description: "Whether this service principal represents a custom SAML application",
Type: schema.TypeBool,
Computed: true,
},

"enterprise_application": {
Description: "Whether this service principal represents an Enterprise Application",
Type: schema.TypeBool,
Computed: true,
},

"gallery_application": {
Description: "Whether this service principal represents a gallery application",
Type: schema.TypeBool,
Computed: true,
},

"visible_to_users": {
Description: "Whether this app is visible to users in My Apps and Office 365 Launcher",
Type: schema.TypeBool,
Computed: true,
},
},
},
},

"homepage_url": {
Description: "Home page or landing page of the application",
Type: schema.TypeString,
Expand Down Expand Up @@ -313,6 +346,7 @@ func servicePrincipalDataSourceRead(ctx context.Context, d *schema.ResourceData,
tf.Set(d, "application_tenant_id", servicePrincipal.AppOwnerOrganizationId)
tf.Set(d, "description", servicePrincipal.Description)
tf.Set(d, "display_name", servicePrincipal.DisplayName)
tf.Set(d, "features", flattenFeatures(servicePrincipal.Tags))
tf.Set(d, "homepage_url", servicePrincipal.Homepage)
tf.Set(d, "logout_url", servicePrincipal.LogoutUrl)
tf.Set(d, "login_url", servicePrincipal.LoginUrl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ func (ServicePrincipalDataSource) testCheckFunc(data acceptance.TestData) resour
check.That(data.ResourceName).Key("application_tenant_id").HasValue(tenantId),
check.That(data.ResourceName).Key("description").HasValue("An internal app for testing"),
check.That(data.ResourceName).Key("display_name").Exists(),
check.That(data.ResourceName).Key("features.#").HasValue("1"),
check.That(data.ResourceName).Key("features.0.custom_single_sign_on_app").HasValue("true"),
check.That(data.ResourceName).Key("features.0.enterprise_application").HasValue("true"),
check.That(data.ResourceName).Key("features.0.gallery_application").HasValue("true"),
check.That(data.ResourceName).Key("features.0.visible_to_users").HasValue("true"),
check.That(data.ResourceName).Key("homepage_url").HasValue(fmt.Sprintf("https://test-%d.internal", data.RandomInteger)),
check.That(data.ResourceName).Key("login_url").HasValue(fmt.Sprintf("https://test-%d.internal/login", data.RandomInteger)),
check.That(data.ResourceName).Key("logout_url").HasValue(fmt.Sprintf("https://test-%d.internal/logout", data.RandomInteger)),
Expand Down
66 changes: 60 additions & 6 deletions internal/services/serviceprincipals/service_principal_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,42 @@ func servicePrincipalResource() *schema.Resource {
ValidateFunc: validation.StringLenBetween(0, 1024),
},

"features": {
Description: "Block of features to configure for this service principal using tags",
Type: schema.TypeList,
Optional: true,
Computed: true,
ConflictsWith: []string{"tags"},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"custom_single_sign_on_app": {
Description: "Whether this service principal represents a custom SAML application",
Type: schema.TypeBool,
Optional: true,
},

"enterprise_application": {
Description: "Whether this service principal represents an Enterprise Application",
Type: schema.TypeBool,
Optional: true,
},

"gallery_application": {
Description: "Whether this service principal represents a gallery application",
Type: schema.TypeBool,
Optional: true,
},

"visible_to_users": {
Description: "Whether this app is visible to users in My Apps and Office 365 Launcher",
Type: schema.TypeBool,
Optional: true,
Default: true,
},
},
},
},

"login_url": {
Description: "The URL where the service provider redirects the user to Azure AD to authenticate. Azure AD uses the URL to launch the application from Microsoft 365 or the Azure AD My Apps. When blank, Azure AD performs IdP-initiated sign-on for applications configured with SAML-based single sign-on",
Type: schema.TypeString,
Expand Down Expand Up @@ -134,10 +170,12 @@ func servicePrincipalResource() *schema.Resource {
},

"tags": {
Description: "A set of tags to apply to the service principal",
Type: schema.TypeSet,
Optional: true,
Set: schema.HashString,
Description: "A set of tags to apply to the service principal",
Type: schema.TypeSet,
Optional: true,
Computed: true,
Set: schema.HashString,
ConflictsWith: []string{"features"},
Elem: &schema.Schema{
Type: schema.TypeString,
},
Expand Down Expand Up @@ -308,6 +346,13 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData,
return servicePrincipalResourceUpdate(ctx, d, meta)
}

var tags []string
if v, ok := d.GetOk("features"); ok {
tags = expandFeatures(v.([]interface{}))
} else {
tags = tf.ExpandStringSlice(d.Get("tags").(*schema.Set).List())
}

properties := msgraph.ServicePrincipal{
AccountEnabled: utils.Bool(d.Get("account_enabled").(bool)),
AlternativeNames: tf.ExpandStringSlicePtr(d.Get("alternative_names").(*schema.Set).List()),
Expand All @@ -319,7 +364,7 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData,
NotificationEmailAddresses: tf.ExpandStringSlicePtr(d.Get("notification_email_addresses").(*schema.Set).List()),
PreferredSingleSignOnMode: utils.NullableString(d.Get("preferred_single_sign_on_mode").(string)),
SamlSingleSignOnSettings: expandSamlSingleSignOn(d.Get("saml_single_sign_on").([]interface{})),
Tags: tf.ExpandStringSlicePtr(d.Get("tags").(*schema.Set).List()),
Tags: &tags,
}

// Sort the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice
Expand Down Expand Up @@ -399,6 +444,14 @@ func servicePrincipalResourceUpdate(ctx context.Context, d *schema.ResourceData,
client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient
directoryObjectsClient := meta.(*clients.Client).ServicePrincipals.DirectoryObjectsClient

var tags []string
featuresChanged := d.HasChange("features")
if v, ok := d.GetOk("features"); ok && len(v.([]interface{})) > 0 && featuresChanged {
tags = expandFeatures(v.([]interface{}))
} else {
tags = tf.ExpandStringSlice(d.Get("tags").(*schema.Set).List())
}

properties := msgraph.ServicePrincipal{
DirectoryObject: msgraph.DirectoryObject{
ID: utils.String(d.Id()),
Expand All @@ -412,7 +465,7 @@ func servicePrincipalResourceUpdate(ctx context.Context, d *schema.ResourceData,
NotificationEmailAddresses: tf.ExpandStringSlicePtr(d.Get("notification_email_addresses").(*schema.Set).List()),
PreferredSingleSignOnMode: utils.NullableString(d.Get("preferred_single_sign_on_mode").(string)),
SamlSingleSignOnSettings: expandSamlSingleSignOn(d.Get("saml_single_sign_on").([]interface{})),
Tags: tf.ExpandStringSlicePtr(d.Get("tags").(*schema.Set).List()),
Tags: &tags,
}

if _, err := client.Update(ctx, properties); err != nil {
Expand Down Expand Up @@ -493,6 +546,7 @@ func servicePrincipalResourceRead(ctx context.Context, d *schema.ResourceData, m
tf.Set(d, "application_tenant_id", servicePrincipal.AppOwnerOrganizationId)
tf.Set(d, "description", servicePrincipal.Description)
tf.Set(d, "display_name", servicePrincipal.DisplayName)
tf.Set(d, "features", flattenFeatures(servicePrincipal.Tags))
tf.Set(d, "homepage_url", servicePrincipal.Homepage)
tf.Set(d, "logout_url", servicePrincipal.LogoutUrl)
tf.Set(d, "login_url", servicePrincipal.LoginUrl)
Expand Down
Loading

0 comments on commit e5644fc

Please sign in to comment.