diff --git a/docs/resources/application.md b/docs/resources/application.md index 7b448e246b..d1a82400cc 100644 --- a/docs/resources/application.md +++ b/docs/resources/application.md @@ -277,7 +277,7 @@ The following arguments are supported: `access_token`, `id_token` and `saml2_token` blocks support the following: -* `additional_properties` - List of additional properties of the claim. If a property exists in this list, it modifies the behaviour of the optional claim. +* `additional_properties` - List of additional properties of the claim. If a property exists in this list, it modifies the behaviour of the optional claim. Possible values are: `cloud_displayname`, `dns_domain_and_sam_account_name`, `emit_as_roles`, `include_externally_authenticated_upn_without_hash`, `include_externally_authenticated_upn`, `max_size_limit`, `netbios_domain_and_sam_account_name`, `on_premise_security_identifier`, `sam_account_name`, and `use_guid`. * `essential` - Whether the claim specified by the client is necessary to ensure a smooth authorization experience. * `name` - The name of the optional claim. * `source` - The source of the claim. If `source` is absent, the claim is a predefined optional claim. If `source` is `user`, the value of `name` is the extension property from the user object. diff --git a/docs/resources/application_optional_claims.md b/docs/resources/application_optional_claims.md new file mode 100644 index 0000000000..9cf13f8882 --- /dev/null +++ b/docs/resources/application_optional_claims.md @@ -0,0 +1,82 @@ +--- +subcategory: "Applications" +--- + +# Resource: azuread_application_optional_claims + +Manages optional claims for an application registration. + +This resource is analogous to the `optional_claims` block in the `azuread_application` resource. When using these resources together, you should use the `ignore_changes` [lifecycle meta-argument](https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle) (see example below). + +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `Application.ReadWrite.OwnedBy` or `Application.ReadWrite.All` + +-> When using the `Application.ReadWrite.OwnedBy` application role, the principal being used to run Terraform must be an owner of the application. + +When authenticated with a user principal, this resource may require one of the following directory roles: `Application Administrator` or `Global Administrator` + +## Example Usage + +```terraform +resource "azuread_application_registration" "example" { + display_name = "example" +} + +resource "azuread_application_optional_claims" "example" { + application_id = azuread_application_registration.example.id + + access_token { + name = "myclaim" + } + + access_token { + name = "otherclaim" + } + + id_token { + name = "userclaim" + source = "user" + essential = true + additional_properties = ["emit_as_roles"] + } + + saml2_token { + name = "samlexample" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `access_token` - (Optional) One or more `access_token` blocks as documented below. +* `application_id` - (Required) The resource ID of the application registration. Changing this forces a new resource to be created. +* `id_token` - (Optional) One or more `id_token` blocks as documented below. +* `saml2_token` - (Optional) One or more `saml2_token` blocks as documented below. + +-> At least one of `access_token`, `id_token` or `saml2_token` must be specified + +--- + +`access_token`, `id_token` and `saml2_token` blocks support the following: + +* `additional_properties` - List of additional properties of the claim. If a property exists in this list, it modifies the behaviour of the optional claim. Possible values are: `cloud_displayname`, `dns_domain_and_sam_account_name`, `emit_as_roles`, `include_externally_authenticated_upn_without_hash`, `include_externally_authenticated_upn`, `max_size_limit`, `netbios_domain_and_sam_account_name`, `on_premise_security_identifier`, `sam_account_name`, and `use_guid`. +* `essential` - Whether the claim specified by the client is necessary to ensure a smooth authorization experience. +* `name` - The name of the optional claim. +* `source` - The source of the claim. If `source` is absent, the claim is a predefined optional claim. If `source` is `user`, the value of `name` is the extension property from the user object. + +## Attributes Reference + +No additional attributes are exported. + +## Import + +Application Optional Claims can be imported using the object ID of the application, in the following format. + +```shell +terraform import azuread_application_optional_claims.example /applications/00000000-0000-0000-0000-000000000000 +``` diff --git a/internal/acceptance/check/that.go b/internal/acceptance/check/that.go index cab0d707f0..d24801d024 100644 --- a/internal/acceptance/check/that.go +++ b/internal/acceptance/check/that.go @@ -48,6 +48,17 @@ func That(resourceName string) thatType { } } +// DoesNotExistInAzure validates that the specified resource does not exist within Azure +func (t thatType) DoesNotExistInAzure(testResource types.TestResource) pluginsdk.TestCheckFunc { + return func(s *terraform.State) error { + client, err := testclient.Build(t.tenantId) + if err != nil { + return fmt.Errorf("building client: %+v", err) + } + return helpers.DoesNotExistInAzure(client, testResource, t.resourceName)(s) + } +} + // ExistsInAzure validates that the specified resource exists within Azure func (t thatType) ExistsInAzure(testResource types.TestResource) pluginsdk.TestCheckFunc { return func(s *terraform.State) error { diff --git a/internal/services/applications/application_optional_claims_resource.go b/internal/services/applications/application_optional_claims_resource.go new file mode 100644 index 0000000000..a95ce72384 --- /dev/null +++ b/internal/services/applications/application_optional_claims_resource.go @@ -0,0 +1,381 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package applications + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/terraform-provider-azuread/internal/sdk" + "github.com/hashicorp/terraform-provider-azuread/internal/services/applications/parse" + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/tf/pluginsdk" + "github.com/manicminer/hamilton/msgraph" +) + +type ApplicationOptionalClaimsModel struct { + ApplicationId string `tfschema:"application_id"` + AccessTokens []OptionalClaim `tfschema:"access_token"` + IdTokens []OptionalClaim `tfschema:"id_token"` + Saml2Tokens []OptionalClaim `tfschema:"saml2_token"` +} + +type OptionalClaim struct { + Name string `tfschema:"name"` + Source string `tfschema:"source"` + Essential bool `tfschema:"essential"` + AdditionalProperties []string `tfschema:"additional_properties"` +} + +type ApplicationOptionalClaimsResource struct{} + +func (r ApplicationOptionalClaimsResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return parse.ValidateOptionalClaimsID +} + +var _ sdk.ResourceWithUpdate = ApplicationOptionalClaimsResource{} + +func (r ApplicationOptionalClaimsResource) ResourceType() string { + return "azuread_application_optional_claims" +} + +func (r ApplicationOptionalClaimsResource) ModelObject() interface{} { + return &ApplicationOptionalClaimsModel{} +} + +func (r ApplicationOptionalClaimsResource) Arguments() (ret map[string]*pluginsdk.Schema) { + ret = map[string]*pluginsdk.Schema{ + "application_id": { + Description: "The resource ID of the application to which these optional claims belong", + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: parse.ValidateApplicationID, + }, + + "access_token": schemaOptionalClaims(), + "id_token": schemaOptionalClaims(), + "saml2_token": schemaOptionalClaims(), + } + + atLeastOneOf := []string{"access_token", "id_token", "saml2_token"} + ret["access_token"].AtLeastOneOf = atLeastOneOf + ret["id_token"].AtLeastOneOf = atLeastOneOf + ret["saml2_token"].AtLeastOneOf = atLeastOneOf + + return +} + +func (r ApplicationOptionalClaimsResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (r ApplicationOptionalClaimsResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 10 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Applications.ApplicationsClient + client.BaseClient.DisableRetries = true + defer func() { client.BaseClient.DisableRetries = false }() + + var model ApplicationOptionalClaimsModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + applicationId, err := parse.ParseApplicationID(model.ApplicationId) + if err != nil { + return err + } + + id := parse.NewOptionalClaimsID(applicationId.ApplicationId) + + tf.LockByName(applicationResourceName, id.ApplicationId) + defer tf.UnlockByName(applicationResourceName, id.ApplicationId) + + result, _, err := client.Get(ctx, applicationId.ApplicationId, odata.Query{}) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", applicationId, err) + } + if result == nil { + return fmt.Errorf("retrieving %s: result was nil", applicationId) + } + + // Check for existing optional claims + if claims := result.OptionalClaims; claims != nil { + if claims.AccessToken != nil && len(*claims.AccessToken) > 0 { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + if claims.IdToken != nil && len(*claims.IdToken) > 0 { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + if claims.Saml2Token != nil && len(*claims.Saml2Token) > 0 { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + } + + // Assemble the optional claims + optionalClaims := msgraph.OptionalClaims{} + + if len(model.AccessTokens) > 0 { + accessTokenClaims := make([]msgraph.OptionalClaim, 0) + for _, claim := range model.AccessTokens { + accessTokenClaims = append(accessTokenClaims, msgraph.OptionalClaim{ + Name: pointer.To(claim.Name), + Source: pointer.To(claim.Source), + Essential: pointer.To(claim.Essential), + AdditionalProperties: pointer.To(claim.AdditionalProperties), + }) + } + optionalClaims.AccessToken = &accessTokenClaims + } + + if len(model.IdTokens) > 0 { + idTokenClaims := make([]msgraph.OptionalClaim, 0) + for _, claim := range model.IdTokens { + idTokenClaims = append(idTokenClaims, msgraph.OptionalClaim{ + Name: pointer.To(claim.Name), + Source: pointer.To(claim.Source), + Essential: pointer.To(claim.Essential), + AdditionalProperties: pointer.To(claim.AdditionalProperties), + }) + } + optionalClaims.IdToken = &idTokenClaims + } + + if len(model.Saml2Tokens) > 0 { + saml2TokenClaims := make([]msgraph.OptionalClaim, 0) + for _, claim := range model.Saml2Tokens { + saml2TokenClaims = append(saml2TokenClaims, msgraph.OptionalClaim{ + Name: pointer.To(claim.Name), + Source: pointer.To(claim.Source), + Essential: pointer.To(claim.Essential), + AdditionalProperties: pointer.To(claim.AdditionalProperties), + }) + } + optionalClaims.Saml2Token = &saml2TokenClaims + } + + properties := msgraph.Application{ + DirectoryObject: msgraph.DirectoryObject{ + Id: &id.ApplicationId, + }, + OptionalClaims: &optionalClaims, + } + + if _, err = client.Update(ctx, properties); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (r ApplicationOptionalClaimsResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Applications.ApplicationsClient + client.BaseClient.DisableRetries = true + defer func() { client.BaseClient.DisableRetries = false }() + + id, err := parse.ParseOptionalClaimsID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + applicationId := parse.NewApplicationID(id.ApplicationId) + + tf.LockByName(applicationResourceName, id.ApplicationId) + defer tf.UnlockByName(applicationResourceName, id.ApplicationId) + + result, status, err := client.Get(ctx, id.ApplicationId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("retrieving %s: %+v", id, err) + } + if result == nil { + return fmt.Errorf("retrieving %s: result was nil", id) + } + + if claims := result.OptionalClaims; claims == nil { + return metadata.MarkAsGone(id) + } else if (claims.AccessToken == nil || len(*claims.AccessToken) == 0) && + (claims.IdToken == nil || len(*claims.IdToken) == 0) && + (claims.Saml2Token == nil || len(*claims.Saml2Token) == 0) { + return metadata.MarkAsGone(id) + } + + state := ApplicationOptionalClaimsModel{ + ApplicationId: applicationId.ID(), + } + + if accessTokenClaims := result.OptionalClaims.AccessToken; accessTokenClaims != nil { + for _, claim := range *accessTokenClaims { + state.AccessTokens = append(state.AccessTokens, OptionalClaim{ + Name: pointer.From(claim.Name), + Source: pointer.From(claim.Source), + Essential: pointer.From(claim.Essential), + AdditionalProperties: pointer.From(claim.AdditionalProperties), + }) + } + } + + if idTokenClaims := result.OptionalClaims.IdToken; idTokenClaims != nil { + for _, claim := range *idTokenClaims { + state.IdTokens = append(state.IdTokens, OptionalClaim{ + Name: pointer.From(claim.Name), + Source: pointer.From(claim.Source), + Essential: pointer.From(claim.Essential), + AdditionalProperties: pointer.From(claim.AdditionalProperties), + }) + } + } + + if idTokenClaims := result.OptionalClaims.Saml2Token; idTokenClaims != nil { + for _, claim := range *idTokenClaims { + state.Saml2Tokens = append(state.Saml2Tokens, OptionalClaim{ + Name: pointer.From(claim.Name), + Source: pointer.From(claim.Source), + Essential: pointer.From(claim.Essential), + AdditionalProperties: pointer.From(claim.AdditionalProperties), + }) + } + } + + return metadata.Encode(&state) + }, + } +} + +func (r ApplicationOptionalClaimsResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 10 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Applications.ApplicationsClient + rd := metadata.ResourceData + + id, err := parse.ParseOptionalClaimsID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + var model ApplicationOptionalClaimsModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + tf.LockByName(applicationResourceName, id.ApplicationId) + defer tf.UnlockByName(applicationResourceName, id.ApplicationId) + + applicationId := parse.NewApplicationID(id.ApplicationId) + result, _, err := client.Get(ctx, applicationId.ApplicationId, odata.Query{}) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", applicationId, err) + } + if result == nil || result.OptionalClaims == nil { + return fmt.Errorf("retrieving %s: optionalClaims was nil", applicationId) + } + + // Start with the existing claims, as they must be updated together, then update each type in turn as needed + newOptionalClaims := *result.OptionalClaims + + if rd.HasChange("access_token") { + newAccessTokenClaims := make([]msgraph.OptionalClaim, 0) + for _, claim := range model.AccessTokens { + newAccessTokenClaims = append(newAccessTokenClaims, msgraph.OptionalClaim{ + Name: pointer.To(claim.Name), + Source: pointer.To(claim.Source), + Essential: pointer.To(claim.Essential), + AdditionalProperties: pointer.To(claim.AdditionalProperties), + }) + } + newOptionalClaims.AccessToken = &newAccessTokenClaims + } + + if rd.HasChange("id_token") { + newIdTokenClaims := make([]msgraph.OptionalClaim, 0) + for _, claim := range model.IdTokens { + newIdTokenClaims = append(newIdTokenClaims, msgraph.OptionalClaim{ + Name: pointer.To(claim.Name), + Source: pointer.To(claim.Source), + Essential: pointer.To(claim.Essential), + AdditionalProperties: pointer.To(claim.AdditionalProperties), + }) + } + newOptionalClaims.IdToken = &newIdTokenClaims + } + + if rd.HasChange("saml2_token") { + newSaml2TokenClaims := make([]msgraph.OptionalClaim, 0) + for _, claim := range model.Saml2Tokens { + newSaml2TokenClaims = append(newSaml2TokenClaims, msgraph.OptionalClaim{ + Name: pointer.To(claim.Name), + Source: pointer.To(claim.Source), + Essential: pointer.To(claim.Essential), + AdditionalProperties: pointer.To(claim.AdditionalProperties), + }) + } + newOptionalClaims.Saml2Token = &newSaml2TokenClaims + } + + properties := msgraph.Application{ + DirectoryObject: msgraph.DirectoryObject{ + Id: &applicationId.ApplicationId, + }, + OptionalClaims: &newOptionalClaims, + } + + _, err = client.Update(ctx, properties) + if err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } + + return nil + }, + } +} + +func (r ApplicationOptionalClaimsResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Applications.ApplicationsClient + client.BaseClient.DisableRetries = true + defer func() { client.BaseClient.DisableRetries = false }() + + id, err := parse.ParseOptionalClaimsID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + applicationId := parse.NewApplicationID(id.ApplicationId) + + tf.LockByName(applicationResourceName, id.ApplicationId) + defer tf.UnlockByName(applicationResourceName, id.ApplicationId) + + properties := msgraph.Application{ + DirectoryObject: msgraph.DirectoryObject{ + Id: &applicationId.ApplicationId, + }, + OptionalClaims: &msgraph.OptionalClaims{}, + } + + _, err = client.Update(ctx, properties) + if err != nil { + return fmt.Errorf("deleting %s: %+v", id, err) + } + + return nil + }, + } +} diff --git a/internal/services/applications/application_optional_claims_resource_test.go b/internal/services/applications/application_optional_claims_resource_test.go new file mode 100644 index 0000000000..8c43413104 --- /dev/null +++ b/internal/services/applications/application_optional_claims_resource_test.go @@ -0,0 +1,253 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package applications_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/services/applications/parse" +) + +type ApplicationOptionalClaimsResource struct{} + +func TestAccApplicationOptionalClaims_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application_optional_claims", "test") + r := ApplicationOptionalClaimsResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("application_id").Exists(), + ), + }, + data.ImportStep(), + { + Config: r.applicationOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That("azuread_application_registration.test").DoesNotExistInAzure(r), + ), + }, + }) +} + +func TestAccApplicationOptionalClaims_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application_optional_claims", "test") + r := ApplicationOptionalClaimsResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("application_id").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplicationOptionalClaims_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application_optional_claims", "test") + r := ApplicationOptionalClaimsResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("application_id").Exists(), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("application_id").Exists(), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("application_id").Exists(), + ), + }, + data.ImportStep(), + { + Config: r.applicationOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That("azuread_application_registration.test").DoesNotExistInAzure(r), + ), + }, + }) +} + +func TestAccApplicationOptionalClaims_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application_optional_claims", "test") + r := ApplicationOptionalClaimsResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("application_id").Exists(), + ), + }, + data.RequiresImportErrorStep(r.requiresImport(data)), + }) +} + +func (r ApplicationOptionalClaimsResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.Applications.ApplicationsClient + client.BaseClient.DisableRetries = true + defer func() { client.BaseClient.DisableRetries = false }() + + id, err := parse.ParseOptionalClaimsID(state.ID) + if err != nil { + return nil, err + } + + result, status, err := client.Get(ctx, id.ApplicationId, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return pointer.To(false), nil + } + return nil, fmt.Errorf("retrieving %s: %+v", id, err) + } + if result == nil { + return nil, fmt.Errorf("retrieving %s: result was nil", id) + } + + if claims := result.OptionalClaims; claims == nil { + return pointer.To(false), nil + } else if (claims.AccessToken == nil || len(*claims.AccessToken) == 0) && + (claims.IdToken == nil || len(*claims.IdToken) == 0) && + (claims.Saml2Token == nil || len(*claims.Saml2Token) == 0) { + return pointer.To(false), nil + } + + return pointer.To(true), nil +} + +func (ApplicationOptionalClaimsResource) applicationOnly(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_application_registration" "test" { + display_name = "acctest-OptionalClaims-%[1]d" +} +`, data.RandomInteger) +} + +func (ApplicationOptionalClaimsResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_application_registration" "test" { + display_name = "acctest-OptionalClaims-%[1]d" +} + +resource "azuread_application_optional_claims" "test" { + application_id = azuread_application_registration.test.id + + access_token { + name = "myclaim" + } + + id_token { + name = "userclaim" + } +} +`, data.RandomInteger) +} + +func (ApplicationOptionalClaimsResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_application_registration" "test" { + display_name = "acctest-OptionalClaims-%[1]d" +} + +resource "azuread_application_optional_claims" "test" { + application_id = azuread_application_registration.test.id + + access_token { + name = "userclaim" + source = "user" + essential = true + additional_properties = ["emit_as_roles"] + } + + access_token { + name = "otherclaim" + essential = false + } + + id_token { + name = "idclaim" + source = "user" + essential = true + additional_properties = ["emit_as_roles"] + } + + saml2_token { + name = "saml2claim" + source = "user" + essential = true + additional_properties = [ + "dns_domain_and_sam_account_name", + "on_premise_security_identifier", + ] + } + + saml2_token { + name = "saml2claim2" + source = "user" + essential = true + additional_properties = ["netbios_domain_and_sam_account_name"] + } +} +`, data.RandomInteger) +} + +func (ApplicationOptionalClaimsResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_application_registration" "test" { + display_name = "acctest-OptionalClaims-%[1]d" +} + +resource "azuread_application_optional_claims" "test" { + application_id = azuread_application_registration.test.id + + access_token { + name = "myclaim" + } +} + +resource "azuread_application_optional_claims" "import" { + application_id = azuread_application_optional_claims.test.application_id + + access_token { + name = "myclaim" + } +} +`, data.RandomInteger, data.RandomID) +} diff --git a/internal/services/applications/parse/optional_claims.go b/internal/services/applications/parse/optional_claims.go new file mode 100644 index 0000000000..79912329fa --- /dev/null +++ b/internal/services/applications/parse/optional_claims.go @@ -0,0 +1,68 @@ +package parse + +import ( + "fmt" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +type OptionalClaimsId struct { + ApplicationId string +} + +func NewOptionalClaimsID(applicationId string) OptionalClaimsId { + return OptionalClaimsId{ + ApplicationId: applicationId, + } +} + +// ParseOptionalClaimsID parses 'input' into an OptionalClaimsId +func ParseOptionalClaimsID(input string) (*OptionalClaimsId, error) { + parser := resourceids.NewParserFromResourceIdType(OptionalClaimsId{}) + parsed, err := parser.Parse(input, false) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + var ok bool + id := OptionalClaimsId{} + + if id.ApplicationId, ok = parsed.Parsed["applicationId"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "applicationId", *parsed) + } + + return &id, nil +} + +// ValidateOptionalClaimsID checks that 'input' can be parsed as an Application ID +func ValidateOptionalClaimsID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := ParseOptionalClaimsID(v); err != nil { + errors = append(errors, err) + return + } + + return +} + +func (id OptionalClaimsId) ID() string { + fmtString := "/applications/%s" + return fmt.Sprintf(fmtString, id.ApplicationId) +} + +// Segments returns a slice of Resource ID Segments which comprise this ID +func (id OptionalClaimsId) Segments() []resourceids.Segment { + return []resourceids.Segment{ + resourceids.StaticSegment("applications", "applications", "applications"), + resourceids.UserSpecifiedSegment("applicationId", "00000000-0000-0000-0000-000000000000"), + } +} + +func (id OptionalClaimsId) String() string { + return fmt.Sprintf("Application Optional Claims (Application ID: %q)", id.ApplicationId) +} diff --git a/internal/services/applications/parse/redirect_uri.go b/internal/services/applications/parse/redirect_uris.go similarity index 100% rename from internal/services/applications/parse/redirect_uri.go rename to internal/services/applications/parse/redirect_uris.go diff --git a/internal/services/applications/registration.go b/internal/services/applications/registration.go index 82afc75ca2..9a70e66105 100644 --- a/internal/services/applications/registration.go +++ b/internal/services/applications/registration.go @@ -56,6 +56,7 @@ func (r Registration) Resources() []sdk.Resource { ApplicationFromTemplateResource{}, ApplicationIdentifierUriResource{}, ApplicationKnownClientsResource{}, + ApplicationOptionalClaimsResource{}, ApplicationOwnerResource{}, ApplicationPermissionScopeResource{}, ApplicationRedirectUrisResource{},