From 9b0213cc163f6cecd5dc0813e8a40e7c24782b96 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Tue, 17 Oct 2023 15:02:02 +0100 Subject: [PATCH] New Resource: `azuread_application_fallback_public_client` --- ...ication_fallback_public_client_resource.go | 169 ++++++++++++++++++ ...on_fallback_public_client_resource_test.go | 131 ++++++++++++++ .../parse/fallback_public_client.go | 70 ++++++++ .../services/applications/registration.go | 1 + 4 files changed, 371 insertions(+) create mode 100644 internal/services/applications/application_fallback_public_client_resource.go create mode 100644 internal/services/applications/application_fallback_public_client_resource_test.go create mode 100644 internal/services/applications/parse/fallback_public_client.go diff --git a/internal/services/applications/application_fallback_public_client_resource.go b/internal/services/applications/application_fallback_public_client_resource.go new file mode 100644 index 0000000000..b252fdaf04 --- /dev/null +++ b/internal/services/applications/application_fallback_public_client_resource.go @@ -0,0 +1,169 @@ +// 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" +) + +type ApplicationFallbackPublicClientModel struct { + ApplicationId string `tfschema:"application_id"` + Enabled bool `tfschema:"enabled"` +} + +type ApplicationFallbackPublicClientResource struct{} + +func (r ApplicationFallbackPublicClientResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return parse.ValidateFallbackPublicClientID +} + +var _ sdk.Resource = ApplicationFallbackPublicClientResource{} + +func (r ApplicationFallbackPublicClientResource) ResourceType() string { + return "azuread_application_fallback_public_client" +} + +func (r ApplicationFallbackPublicClientResource) ModelObject() interface{} { + return &ApplicationFallbackPublicClientModel{} +} + +func (r ApplicationFallbackPublicClientResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "application_id": { + Description: "The resource ID of the application to which the fallback public client setting should be applied", + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: parse.ValidateApplicationID, + }, + + "enabled": { + Description: "Specifies explicitly whether the application is a public client. Appropriate for apps using token grant flows that don't use a redirect URI", + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + }, + } +} + +func (r ApplicationFallbackPublicClientResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (r ApplicationFallbackPublicClientResource) 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 ApplicationFallbackPublicClientModel + 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.NewFallbackPublicClientID(applicationId.ApplicationId) + + tf.LockByName(applicationResourceName, id.ApplicationId) + defer tf.UnlockByName(applicationResourceName, id.ApplicationId) + + if _, err = client.SetFallbackPublicClient(ctx, id.ApplicationId, pointer.To(model.Enabled)); err != nil { + return fmt.Errorf("setting %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (r ApplicationFallbackPublicClientResource) 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.ParseFallbackPublicClientID(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 result.IsFallbackPublicClient == nil { + return metadata.MarkAsGone(id) + } + + state := ApplicationFallbackPublicClientModel{ + ApplicationId: applicationId.ID(), + Enabled: pointer.From(result.IsFallbackPublicClient), + } + + return metadata.Encode(&state) + }, + } +} + +func (r ApplicationFallbackPublicClientResource) 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.ParseFallbackPublicClientID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + var model ApplicationFallbackPublicClientModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + tf.LockByName(applicationResourceName, id.ApplicationId) + defer tf.UnlockByName(applicationResourceName, id.ApplicationId) + + _, err = client.SetFallbackPublicClient(ctx, id.ApplicationId, nil) + if err != nil { + return fmt.Errorf("unsetting %s: %+v", id, err) + } + + return nil + }, + } +} diff --git a/internal/services/applications/application_fallback_public_client_resource_test.go b/internal/services/applications/application_fallback_public_client_resource_test.go new file mode 100644 index 0000000000..c767259337 --- /dev/null +++ b/internal/services/applications/application_fallback_public_client_resource_test.go @@ -0,0 +1,131 @@ +// 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 ApplicationFallbackPublicClientResource struct{} + +func TestAccApplicationFallbackPublicClient_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application_fallback_public_client", "test") + r := ApplicationFallbackPublicClientResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.enabled(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("application_id").Exists(), + check.That(data.ResourceName).Key("enabled").HasValue("true"), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplicationFallbackPublicClient_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application_fallback_public_client", "test") + r := ApplicationFallbackPublicClientResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.disabled(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("application_id").Exists(), + check.That(data.ResourceName).Key("enabled").HasValue("false"), + ), + }, + data.ImportStep(), + { + Config: r.enabled(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("application_id").Exists(), + check.That(data.ResourceName).Key("enabled").HasValue("true"), + ), + }, + data.ImportStep(), + { + Config: r.disabled(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("application_id").Exists(), + check.That(data.ResourceName).Key("enabled").HasValue("false"), + ), + }, + data.ImportStep(), + }) +} + +func (r ApplicationFallbackPublicClientResource) 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.ParseFallbackPublicClientID(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 result.IsFallbackPublicClient == nil { + return pointer.To(false), nil + } + + return pointer.To(true), nil +} + +func (ApplicationFallbackPublicClientResource) enabled(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_application_registration" "test" { + display_name = "acctest-FallbackPublicClient-%[1]d" +} + +resource "azuread_application_fallback_public_client" "test" { + application_id = azuread_application_registration.test.id + enabled = true +} +`, data.RandomInteger, data.RandomID) +} + +func (ApplicationFallbackPublicClientResource) disabled(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_application_registration" "test" { + display_name = "acctest-FallbackPublicClient-%[1]d" +} + +resource "azuread_application_fallback_public_client" "test" { + application_id = azuread_application_registration.test.id + enabled = false +} +`, data.RandomInteger) +} diff --git a/internal/services/applications/parse/fallback_public_client.go b/internal/services/applications/parse/fallback_public_client.go new file mode 100644 index 0000000000..6900796847 --- /dev/null +++ b/internal/services/applications/parse/fallback_public_client.go @@ -0,0 +1,70 @@ +package parse + +import ( + "fmt" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" + "github.com/hashicorp/terraform-provider-azuread/internal/tf/validation" +) + +type FallbackPublicClientId struct { + ApplicationId string +} + +func NewFallbackPublicClientID(applicationId string) FallbackPublicClientId { + return FallbackPublicClientId{ + ApplicationId: applicationId, + } +} + +// ParseFallbackPublicClientID parses 'input' into an FallbackPublicClientId +func ParseFallbackPublicClientID(input string) (*FallbackPublicClientId, error) { + parser := resourceids.NewParserFromResourceIdType(FallbackPublicClientId{}) + parsed, err := parser.Parse(input, false) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + var ok bool + id := FallbackPublicClientId{} + + if id.ApplicationId, ok = parsed.Parsed["applicationId"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "applicationId", *parsed) + } + + return &id, nil +} + +// ValidateFallbackPublicClientID checks that 'input' can be parsed as an Application ID +func ValidateFallbackPublicClientID(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 + } + + id, err := ParseFallbackPublicClientID(v) + if err != nil { + errors = append(errors, err) + } + + return validation.IsUUID(id.ApplicationId, "ID") +} + +func (id FallbackPublicClientId) ID() string { + fmtString := "/applications/%s/fallbackPublicClient" + return fmt.Sprintf(fmtString, id.ApplicationId) +} + +// Segments returns a slice of Resource ID Segments which comprise this B 2 C Directory ID +func (id FallbackPublicClientId) Segments() []resourceids.Segment { + return []resourceids.Segment{ + resourceids.StaticSegment("applications", "applications", "applications"), + resourceids.UserSpecifiedSegment("applicationId", "00000000-0000-0000-0000-000000000000"), + resourceids.StaticSegment("fallbackPublicClient", "fallbackPublicClient", "fallbackPublicClient"), + } +} + +func (id FallbackPublicClientId) String() string { + return fmt.Sprintf("Fallback Public Client (Application ID: %q)", id.ApplicationId) +} diff --git a/internal/services/applications/registration.go b/internal/services/applications/registration.go index 65317559dc..ced6b74a71 100644 --- a/internal/services/applications/registration.go +++ b/internal/services/applications/registration.go @@ -51,6 +51,7 @@ func (r Registration) DataSources() []sdk.DataSource { func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ ApplicationAppRoleResource{}, + ApplicationFallbackPublicClientResource{}, ApplicationPermissionScopeResource{}, ApplicationRegistrationResource{}, }