diff --git a/azuread/resource_application.go b/azuread/resource_application.go index 947f30b73..b18c1e145 100644 --- a/azuread/resource_application.go +++ b/azuread/resource_application.go @@ -5,10 +5,9 @@ import ( "log" "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" - + "github.com/google/uuid" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/validation" - "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/ar" "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/graph" "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/p" @@ -94,6 +93,56 @@ func resourceApplication() *schema.Resource { Default: "webapp/api", }, + "app_role": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + + "allowed_member_types": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice( + []string{"User", "Application"}, + false, + ), + }, + }, + + "description": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validate.NoEmptyStrings, + }, + + "display_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validate.NoEmptyStrings, + }, + + "is_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "value": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validate.NoEmptyStrings, + }, + }, + }, + }, + "required_resource_access": { Type: schema.TypeSet, Optional: true, @@ -159,6 +208,7 @@ func resourceApplicationCreate(d *schema.ResourceData, meta interface{}) error { ReplyUrls: tf.ExpandStringSlicePtr(d.Get("reply_urls").(*schema.Set).List()), AvailableToOtherTenants: p.Bool(d.Get("available_to_other_tenants").(bool)), RequiredResourceAccess: expandADApplicationRequiredResourceAccess(d), + AppRoles: expandADApplicationAppRoles(d.Get("app_role")), } if v, ok := d.GetOk("homepage"); ok { @@ -251,6 +301,32 @@ func resourceApplicationUpdate(d *schema.ResourceData, meta interface{}) error { properties.RequiredResourceAccess = expandADApplicationRequiredResourceAccess(d) } + if d.HasChange("app_role") { + // if the app role already exists then it must be disabled + // with no other changes before it can be edited or deleted + var app graphrbac.Application + var appRolesProperties graphrbac.ApplicationUpdateParameters + resp, err := client.Get(ctx, d.Id()) + if err != nil { + if ar.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Error: AzureAD Application with ID %q was not found", d.Id()) + } + + return fmt.Errorf("Error making Read request on AzureAD Application with ID %q: %+v", d.Id(), err) + } + app = resp + for _, appRole := range *app.AppRoles { + *appRole.IsEnabled = false + } + appRolesProperties.AppRoles = app.AppRoles + if _, err := client.Patch(ctx, d.Id(), appRolesProperties); err != nil { + return fmt.Errorf("Error disabling App Roles for Azure AD Application with ID %q: %+v", d.Id(), err) + } + + // now we can set the new state of the app role + properties.AppRoles = expandADApplicationAppRoles(d.Get("app_role")) + } + if d.HasChange("group_membership_claims") { properties.GroupMembershipClaims = d.Get("group_membership_claims") } @@ -265,7 +341,6 @@ func resourceApplicationUpdate(d *schema.ResourceData, meta interface{}) error { properties.IdentifierUris = &[]string{} default: return fmt.Errorf("Error paching Azure AD Application with ID %q: Unknow application type %v. Supported types are [webapp/api, native]", d.Id(), appType) - } } @@ -320,6 +395,10 @@ func resourceApplicationRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Error setting `required_resource_access`: %+v", err) } + if err := d.Set("app_role", flattenADApplicationAppRoles(app.AppRoles)); err != nil { + return fmt.Errorf("Error setting `app_role`: %+v", err) + } + if err := d.Set("oauth2_permissions", graph.FlattenOauth2Permissions(app.Oauth2Permissions)); err != nil { return fmt.Errorf("Error setting `oauth2_permissions`: %+v", err) } @@ -432,3 +511,76 @@ func flattenADApplicationResourceAccess(in *[]graphrbac.ResourceAccess) []interf return accesses } + +func expandADApplicationAppRoles(i interface{}) *[]graphrbac.AppRole { + input := i.(*schema.Set).List() + if len(input) == 0 { + return nil + } + + var output []graphrbac.AppRole + + for _, appRoleRaw := range input { + appRole := appRoleRaw.(map[string]interface{}) + + appRoleID := appRole["id"].(string) + if appRoleID == "" { + appRoleID = uuid.New().String() + } + + var appRoleAllowedMemberTypes []string + for _, appRoleAllowedMemberType := range appRole["allowed_member_types"].(*schema.Set).List() { + appRoleAllowedMemberTypes = append(appRoleAllowedMemberTypes, appRoleAllowedMemberType.(string)) + } + + appRoleDescription := appRole["description"].(string) + appRoleDisplayName := appRole["display_name"].(string) + appRoleIsEnabled := appRole["is_enabled"].(bool) + appRoleValue := appRole["value"].(string) + + output = append(output, + graphrbac.AppRole{ + ID: &appRoleID, + AllowedMemberTypes: &appRoleAllowedMemberTypes, + Description: &appRoleDescription, + DisplayName: &appRoleDisplayName, + IsEnabled: &appRoleIsEnabled, + Value: &appRoleValue, + }, + ) + } + + return &output +} + +func flattenADApplicationAppRoles(in *[]graphrbac.AppRole) []interface{} { + if in == nil { + return []interface{}{} + } + + appRoles := make([]interface{}, 0) + for _, role := range *in { + appRole := make(map[string]interface{}) + if role.ID != nil { + appRole["id"] = *role.ID + } + if role.AllowedMemberTypes != nil { + appRole["allowed_member_types"] = *role.AllowedMemberTypes + } + if role.Description != nil { + appRole["description"] = *role.Description + } + if role.DisplayName != nil { + appRole["display_name"] = *role.DisplayName + } + if role.IsEnabled != nil { + appRole["is_enabled"] = *role.IsEnabled + } + if role.Value != nil { + appRole["value"] = *role.Value + } + appRoles = append(appRoles, appRole) + } + + return appRoles +} diff --git a/azuread/resource_application_test.go b/azuread/resource_application_test.go index 6edf13730..6974d7d0a 100644 --- a/azuread/resource_application_test.go +++ b/azuread/resource_application_test.go @@ -8,7 +8,6 @@ import ( "github.com/google/uuid" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" - "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/ar" ) @@ -138,6 +137,111 @@ func TestAccAzureADApplication_availableToOtherTenants(t *testing.T) { }) } +func TestAccAzureADApplication_appRoles(t *testing.T) { + resourceName := "azuread_application.test" + id := uuid.New().String() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckADApplicationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccADApplication_appRoles(id), + Check: resource.ComposeTestCheckFunc( + testCheckADApplicationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "app_role.#", "1"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.allowed_member_types.#", "2"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.allowed_member_types.2550101162", "Application"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.allowed_member_types.2906997583", "User"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.description", "Admins can manage roles and perform all task actions"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.display_name", "Admin"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.is_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.value", "Admin"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureADApplication_appRolesUpdate(t *testing.T) { + resourceName := "azuread_application.test" + id := uuid.New().String() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckADApplicationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccADApplication_appRoles(id), + Check: resource.ComposeTestCheckFunc( + testCheckADApplicationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "app_role.#", "1"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.allowed_member_types.#", "2"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.allowed_member_types.2550101162", "Application"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.allowed_member_types.2906997583", "User"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.description", "Admins can manage roles and perform all task actions"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.display_name", "Admin"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.is_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "app_role.3282540397.value", "Admin"), + ), + }, + { + Config: testAccADApplication_appRolesUpdate(id), + Check: resource.ComposeTestCheckFunc( + testCheckADApplicationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "app_role.#", "2"), + resource.TestCheckResourceAttr(resourceName, "app_role.1786747921.allowed_member_types.#", "1"), + resource.TestCheckResourceAttr(resourceName, "app_role.1786747921.allowed_member_types.2906997583", "User"), + resource.TestCheckResourceAttr(resourceName, "app_role.1786747921.description", "ReadOnly roles have limited query access"), + resource.TestCheckResourceAttr(resourceName, "app_role.1786747921.display_name", "ReadOnly"), + resource.TestCheckResourceAttr(resourceName, "app_role.1786747921.is_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "app_role.1786747921.value", "User"), + resource.TestCheckResourceAttr(resourceName, "app_role.2608972077.allowed_member_types.#", "1"), + resource.TestCheckResourceAttr(resourceName, "app_role.2608972077.allowed_member_types.2906997583", "User"), + resource.TestCheckResourceAttr(resourceName, "app_role.2608972077.description", "Admins can manage roles and perform all task actions"), + resource.TestCheckResourceAttr(resourceName, "app_role.2608972077.display_name", "Admin"), + resource.TestCheckResourceAttr(resourceName, "app_role.2608972077.is_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "app_role.2608972077.value", "Admin"), + ), + }, + }, + }) +} + +func TestAccAzureADApplication_appRolesDelete(t *testing.T) { + resourceName := "azuread_application.test" + id := uuid.New().String() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckADApplicationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccADApplication_appRolesUpdate(id), + Check: resource.ComposeTestCheckFunc( + testCheckADApplicationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "app_role.#", "2"), + ), + }, + { + Config: testAccADApplication_appRoles(id), + Check: resource.ComposeTestCheckFunc( + testCheckADApplicationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "app_role.#", "1"), + ), + }, + }, + }) +} + func TestAccAzureADApplication_groupMembershipClaimsUpdate(t *testing.T) { resourceName := "azuread_application.test" id := uuid.New().String() @@ -436,6 +540,47 @@ resource "azuread_application" "test" { `, id, id, id) } +func testAccADApplication_appRoles(id string) string { + return fmt.Sprintf(` +resource "azuread_application" "test" { + name = "acctest%s" + app_role { + allowed_member_types = [ + "User", + "Application", + ] + description = "Admins can manage roles and perform all task actions" + display_name = "Admin" + is_enabled = true + value = "Admin" + } +} +`, id) +} + +func testAccADApplication_appRolesUpdate(id string) string { + return fmt.Sprintf(` +resource "azuread_application" "test" { + name = "acctest%s" + app_role { + allowed_member_types = ["User"] + description = "Admins can manage roles and perform all task actions" + display_name = "Admin" + is_enabled = true + value = "Admin" + } + + app_role { + allowed_member_types = ["User"] + description = "ReadOnly roles have limited query access" + display_name = "ReadOnly" + is_enabled = true + value = "User" + } +} +`, id) +} + func testAccADApplication_native(id string) string { return fmt.Sprintf(` resource "azuread_application" "test" { diff --git a/website/docs/r/application.html.markdown b/website/docs/r/application.html.markdown index 2b43fb11b..2ce2f62b2 100644 --- a/website/docs/r/application.html.markdown +++ b/website/docs/r/application.html.markdown @@ -51,6 +51,18 @@ resource "azuread_application" "test" { type = "Scope" } } + + app_role { + allowed_member_types = [ + "User", + "Application", + ] + + description = "Admins can manage roles and perform all task actions" + display_name = "Admin" + is_enabled = true + value = "Admin" + } } ``` @@ -74,7 +86,9 @@ The following arguments are supported: * `required_resource_access` - (Optional) A collection of `required_resource_access` blocks as documented below. -* `type` - (Optional) Type of an application: `webapp/api` or `native`. Defaults to `webapp/api`. For `native` apps type `identifier_uris` property can not not be set. +* `type` - (Optional) Type of an application: `webapp/api` or `native`. Defaults to `webapp/api`. For `native` apps type `identifier_uris` property can not not be set. + +* `app_role` - (Optional) A collection of `app_role` blocks as documented below. For more information https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/app-roles --- @@ -88,10 +102,26 @@ The following arguments are supported: `resource_access` supports the following: -* `id` - (Required) The unique identifier for one of the `OAuth2Permission` or `AppRole` instances that the resource application exposes. +* `id` - (Required) The unique identifier for one of the `OAuth2Permission` or `AppRole` instances that the resource application exposes. * `type` - (Required) Specifies whether the id property references an `OAuth2Permission` or an `AppRole`. Possible values are `Scope` or `Role`. +--- + +`app_role` supports the following: + +* `id` - The unique identifier of the `app_role`. + +* `allowed_member_types` - (Required) Specifies whether this app role definition can be assigned to users and groups by setting to `User`, or to other applications (that are accessing this application in daemon service scenarios) by setting to `Application`, or to both. + +* `description` - (Required) Permission help text that appears in the admin app assignment and consent experiences. + +* `display_name` - (Required) Display name for the permission that appears in the admin consent and app assignment experiences. + +* `is_enabled` - (Optional) Determines if the app role is enabled: Defaults to `true`. + +* `value` - (Required) Specifies the value of the roles claim that the application should expect in the authentication and access tokens. + ## Attributes Reference The following attributes are exported: