Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
subcategory: "Applications"
---

# Resource: azuread_application_flexible_federated_identity_credential

Manages a flexible federated identity credential associated with an application within Azure Active Directory.

## 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 requires 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_flexible_federated_identity_credential" "example" {
application_id = azuread_application_registration.example.id
claims_matching_expression = "claims['sub'] matches 'repo:contoso/contoso-repo:ref:refs/heads/*' and claims['job_workflow_ref'] matches 'contoso/contoso-prod/.github/workflows/*.yml@refs/heads/main'"
display_name = "my-repo-deploy"
description = "Deployments for my-repo"
audience = "api://AzureADTokenExchange"
issuer = "https://token.actions.githubusercontent.com"
}
```

## Argument Reference

The following arguments are supported:

* `application_id` - (Required) The resource ID of the application for which this federated identity credential should be created. Changing this field forces a new resource to be created.
* `audience` - (Required) The audience that can appear in the external token. This specifies what should be accepted in the `aud` claim of incoming tokens.
* `claims_matching_expression` - (Required) The expression to match for claims. See the [Preview Documentation](https://learn.microsoft.com/en-us/entra/workload-id/workload-identities-flexible-federated-identity-credentials?tabs=terraformcloud#flexible-federated-identity-credential-expression-language-functionality) for more information.
* `description` - (Optional) A description for the federated identity credential.
* `display_name` - (Required) A unique display name for the federated identity credential. Changing this forces a new resource to be created.
* `issuer` - (Required) The URL of the external identity provider, which must match the issuer claim of the external token being exchanged.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:

* `credential_id` - A UUID used to uniquely identify this federated identity credential.

## Timeouts

The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions:

* `create` - (Defaults to 15 minutes) Used when creating the resource.
* `read` - (Defaults to 5 minutes) Used when retrieving the resource.
* `update` - (Defaults to 5 minutes) Used when updating the resource.
* `delete` - (Defaults to 5 minutes) Used when deleting the resource.

## Import

Flexible Federated Identity Credentials can be imported using the object ID of the associated application and the ID of the flexible federated identity credential, e.g.

```shell
terraform import azuread_application_flexible_federated_identity_credential.example 00000000-0000-0000-0000-000000000000/federatedIdentityCredential/11111111-1111-1111-1111-111111111111
```

-> This ID format is unique to Terraform and is composed of the application's object ID, the string "federatedIdentityCredential" and the credential ID in the format `{ObjectId}/federatedIdentityCredential/{CredentialId}`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package applications

import (
"context"
"fmt"
"time"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-sdk/microsoft-graph/applications/beta/application"
"github.com/hashicorp/go-azure-sdk/microsoft-graph/applications/beta/federatedidentitycredential"
"github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/beta"
"github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/stable"
"github.com/hashicorp/go-azure-sdk/sdk/client/pollers"
"github.com/hashicorp/go-azure-sdk/sdk/nullable"
"github.com/hashicorp/terraform-provider-azuread/internal/helpers/consistency"
"github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf"
"github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk"
"github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/validation"
"github.com/hashicorp/terraform-provider-azuread/internal/sdk"
"github.com/hashicorp/terraform-provider-azuread/internal/services/applications/custompollers"
)

type flexibleFederatedIdentityCredentialResource struct{}

var _ sdk.ResourceWithUpdate = &flexibleFederatedIdentityCredentialResource{}

type flexibleFederatedIdentityCredentialModel struct {
ApplicationId string `tfschema:"application_id"`
Audience string `tfschema:"audience"`
ClaimsMatchingExpression string `tfschema:"claims_matching_expression"`
Description string `tfschema:"description"`
DisplayName string `tfschema:"display_name"`
Issuer string `tfschema:"issuer"`
CredentialId string `tfschema:"credential_id"`
}

func (f flexibleFederatedIdentityCredentialResource) Arguments() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"application_id": {
Description: "The resource ID of the application for which this flexible federated identity credential should be created",
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: stable.ValidateApplicationID,
},

"audience": {
Description: "The audience that can appear in the external token. This specifies what should be accepted in the `aud` claim of incoming tokens.",
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringLenBetween(1, 600),
},

"claims_matching_expression": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotWhiteSpace,
Description: "The expression to match for claims.",
},

"display_name": {
Description: "A unique display name for the flexible federated identity credential",
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringLenBetween(1, 120),
},

"issuer": {
Description: "The URL of the external identity provider, which must match the issuer claim of the external token being exchanged. The combination of the values of issuer and subject must be unique on the app.",
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotWhiteSpace,
},

"description": {
Description: "A description for the flexible federated identity credential",
Type: pluginsdk.TypeString,
Optional: true,
ValidateFunc: validation.StringLenBetween(0, 600),
},
}
}

func (f flexibleFederatedIdentityCredentialResource) Attributes() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"credential_id": {
Description: "A UUID used to uniquely identify this flexible federated identity credential",
Type: pluginsdk.TypeString,
Computed: true,
},
}
}

func (f flexibleFederatedIdentityCredentialResource) ModelObject() interface{} {
return &flexibleFederatedIdentityCredentialModel{}
}

func (f flexibleFederatedIdentityCredentialResource) ResourceType() string {
return "azuread_application_flexible_federated_identity_credential"
}

func (f flexibleFederatedIdentityCredentialResource) Create() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 15 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Applications.ApplicationClientBeta
flexibleFederatedIdentityCredentialClient := metadata.Client.Applications.ApplicationFlexibleFederatedIdentityCredential

data := &flexibleFederatedIdentityCredentialModel{}

if err := metadata.Decode(data); err != nil {
return fmt.Errorf("decoding: %+v", err)
}

applicationId, err := beta.ParseApplicationID(data.ApplicationId)
if err != nil {
return err
}

tf.LockByName(applicationResourceName, applicationId.ApplicationId)
defer tf.UnlockByName(applicationResourceName, applicationId.ApplicationId)

resp, err := client.GetApplication(ctx, *applicationId, application.DefaultGetApplicationOperationOptions())
if err != nil {
return fmt.Errorf("retrieving %s: %+v", applicationId, err)
}

app := resp.Model
if app == nil {
return fmt.Errorf("retrieving %s: model was nil", applicationId)
}

credential := beta.FederatedIdentityCredential{
Audiences: []string{data.Audience},
ClaimsMatchingExpression: &beta.FederatedIdentityExpression{
Value: data.ClaimsMatchingExpression,
LanguageVersion: 1, // Note - from docs: the language version to be used. Should always be set to 1, and is required to be set/sent.
},
Description: nullable.Value(data.Description),
Issuer: data.Issuer,
Name: data.DisplayName,
}

federatedIdentityCredentialResp, err := flexibleFederatedIdentityCredentialClient.CreateFederatedIdentityCredential(ctx, *applicationId, credential, federatedidentitycredential.DefaultCreateFederatedIdentityCredentialOperationOptions())
if err != nil {
return fmt.Errorf("adding flexible federated identity credential for %s", applicationId)
}

newCredential := federatedIdentityCredentialResp.Model
if newCredential == nil {
return fmt.Errorf("api error adding flexible federated identity credential for %s. nil credential received when adding flexible federated identity credential", applicationId)
}
if newCredential.Id == nil {
return fmt.Errorf("api error adding flexible federated identity credential for %s. nil or empty ID received", applicationId)
}

id := beta.NewApplicationIdFederatedIdentityCredentialID(applicationId.ApplicationId, *newCredential.Id)

pollerType := custompollers.NewApplicationFlexibleFederatedCredentialCreationPoller(flexibleFederatedIdentityCredentialClient, id)
poller := pollers.NewPoller(pollerType, 10*time.Second, pollers.DefaultNumberOfDroppedConnectionsToAllow)
// Wait for the credential to replicate - TODO This may need converting to a consistency.WaitForUpdate
if err := poller.PollUntilDone(ctx); err != nil {
return err
}

metadata.SetID(id)

return nil
},
}
}

func (f flexibleFederatedIdentityCredentialResource) Read() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
federatedIdentityCredentialClient := metadata.Client.Applications.ApplicationFlexibleFederatedIdentityCredential

state := flexibleFederatedIdentityCredentialModel{}

id, err := beta.ParseApplicationIdFederatedIdentityCredentialID(metadata.ResourceData.Id())
if err != nil {
return err
}

resp, err := federatedIdentityCredentialClient.GetFederatedIdentityCredential(ctx, *id, federatedidentitycredential.DefaultGetFederatedIdentityCredentialOperationOptions())
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
return metadata.MarkAsGone(id)
}

return fmt.Errorf("retrieving %s: %+v", id, err)
}

state.ApplicationId = beta.NewApplicationID(id.ApplicationId).ID()
state.CredentialId = id.FederatedIdentityCredentialId
if model := resp.Model; model != nil {
if model.ClaimsMatchingExpression != nil {
state.ClaimsMatchingExpression = model.ClaimsMatchingExpression.Value
}
if len(model.Audiences) > 0 {
state.Audience = model.Audiences[0]
}
state.Description = model.Description.GetOrZero()
state.DisplayName = model.Name
state.Issuer = model.Issuer
}

return metadata.Encode(&state)
},
}
}

func (f flexibleFederatedIdentityCredentialResource) Update() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
federatedIdentityCredentialClient := metadata.Client.Applications.ApplicationFlexibleFederatedIdentityCredential

id, err := beta.ParseApplicationIdFederatedIdentityCredentialID(metadata.ResourceData.Id())
if err != nil {
return err
}

tf.LockByName(applicationResourceName, id.ApplicationId)
defer tf.UnlockByName(applicationResourceName, id.ApplicationId)

data := &flexibleFederatedIdentityCredentialModel{}

if err := metadata.Decode(data); err != nil {
return fmt.Errorf("decoding %s: %+v", id, err)
}

credential := beta.FederatedIdentityCredential{
Id: pointer.To(id.FederatedIdentityCredentialId),
Audiences: []string{data.Audience},
Description: nullable.Value(data.Description),
Issuer: data.Issuer,
ClaimsMatchingExpression: &beta.FederatedIdentityExpression{
Value: data.ClaimsMatchingExpression,
LanguageVersion: 1, // Note - from docs: the language version to be used. Should always be set to 1, and is required to be set/sent.
},
// Name is immutable but must be specified as it is a required field
Name: data.DisplayName,
}

if _, err = federatedIdentityCredentialClient.UpdateFederatedIdentityCredential(ctx, *id, credential, federatedidentitycredential.DefaultUpdateFederatedIdentityCredentialOperationOptions()); err != nil {
return fmt.Errorf("updating %s: %+v", id, err)
}

return nil
},
}
}

func (f flexibleFederatedIdentityCredentialResource) Delete() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
federatedIdentityCredentialClient := metadata.Client.Applications.ApplicationFlexibleFederatedIdentityCredential

id, err := beta.ParseApplicationIdFederatedIdentityCredentialID(metadata.ResourceData.Id())
if err != nil {
return err
}

tf.LockByName(applicationResourceName, id.ApplicationId)
defer tf.UnlockByName(applicationResourceName, id.ApplicationId)

if _, err = federatedIdentityCredentialClient.DeleteFederatedIdentityCredential(ctx, *id, federatedidentitycredential.DefaultDeleteFederatedIdentityCredentialOperationOptions()); err != nil {
return fmt.Errorf("deleting %s: %+v", id, err)
}

// Wait for credential to be deleted
if err = consistency.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) {
resp, err := federatedIdentityCredentialClient.GetFederatedIdentityCredential(ctx, *id, federatedidentitycredential.DefaultGetFederatedIdentityCredentialOperationOptions())
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
return pointer.To(false), nil
}
return nil, err
}
credential := resp.Model
if credential == nil {
return pointer.To(false), nil
}

return pointer.To(true), nil
}); err != nil {
return fmt.Errorf("waiting for deletion of %s: %+v", id, err)
}

return nil
},
}
}

func (f flexibleFederatedIdentityCredentialResource) IDValidationFunc() pluginsdk.SchemaValidateFunc {
return beta.ValidateApplicationIdFederatedIdentityCredentialID
}
Loading
Loading