From 5d8d3fde77aa3f35a4d63fd2ab6b5ed1a38aef35 Mon Sep 17 00:00:00 2001 From: Alex Wilcox Date: Mon, 14 Aug 2023 14:52:09 +0000 Subject: [PATCH] Auth Strength WIP --- ...authentication_strength_policy_resource.go | 179 ++++++++++++++++++ ...ntication_strength_policy_resource_test.go | 137 ++++++++++++++ .../conditionalaccess/client/client.go | 13 +- .../conditional_access_policy_resource.go | 17 +- .../conditionalaccess/conditionalaccess.go | 5 + .../conditionalaccess/registration.go | 5 +- .../msgraph/authentication_strength_policy.go | 31 +++ 7 files changed, 377 insertions(+), 10 deletions(-) create mode 100644 internal/services/conditionalaccess/authentication_strength_policy_resource.go create mode 100644 internal/services/conditionalaccess/authentication_strength_policy_resource_test.go diff --git a/internal/services/conditionalaccess/authentication_strength_policy_resource.go b/internal/services/conditionalaccess/authentication_strength_policy_resource.go new file mode 100644 index 0000000000..1d626c2855 --- /dev/null +++ b/internal/services/conditionalaccess/authentication_strength_policy_resource.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package conditionalaccess + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "time" + + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers" + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/hashicorp/terraform-provider-azuread/internal/validate" + "github.com/manicminer/hamilton/msgraph" +) + +func authenticationStrengthPolicyResource() *schema.Resource { + return &schema.Resource{ + CreateContext: authenticationStrengthPolicyCreate, + ReadContext: authenticationStrengthPolicyRead, + UpdateContext: authenticationStrengthPolicyUpdate, + DeleteContext: authenticationStrengthPolicyDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Importer: tf.ValidateResourceIDPriorToImport(func(id string) error { + if _, err := uuid.ParseUUID(id); err != nil { + return fmt.Errorf("specified ID (%q) is not valid: %s", id, err) + } + return nil + }), + + Schema: map[string]*schema.Schema{ + + "display_name": { + Description: "The display name for the authentication strength policy", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + + "description": { + Description: "The description for the authentication strength policy", + Type: schema.TypeString, + Optional: true, + }, + + "allowed_combinations": { + Description: "The allowed MFA methods for this policy", + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func authenticationStrengthPolicyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ConditionalAccess.AuthenticationStrengthPoliciesClient + + properties := msgraph.AuthenticationStrengthPolicy{ + DisplayName: utils.String(d.Get("display_name").(string)), + Description: utils.String(d.Get("description").(string)), + AllowedCombinations: tf.ExpandStringSlicePtr(d.Get("allowed_combinations").(*schema.Set).List()), + } + + authenticationStrengthPolicy, _, err := client.Create(ctx, properties) + + if err != nil { + return tf.ErrorDiagF(err, "Could not create authentication strength policy") + } + + d.SetId(*authenticationStrengthPolicy.ID) + + return authenticationStrengthPolicyRead(ctx, d, meta) +} + +func authenticationStrengthPolicyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ConditionalAccess.AuthenticationStrengthPoliciesClient + + properties := msgraph.AuthenticationStrengthPolicy{ + ID: utils.String(d.Id()), + DisplayName: utils.String(d.Get("display_name").(string)), + Description: utils.String(d.Get("description").(string)), + // AllowedCombinations: tf.ExpandStringSlicePtr(d.Get("allowed_combinations").(*schema.Set).List()), + } + + _, err := client.Update(ctx, properties) + + if err != nil { + return tf.ErrorDiagF(err, "Could not update authentication strength policy") + } + + if d.HasChange("allowed_combinations") { + properties.AllowedCombinations = tf.ExpandStringSlicePtr(d.Get("allowed_combinations").(*schema.Set).List()) + _, err := client.UpdateAllowedCombinations(ctx, properties) + if err != nil { + return tf.ErrorDiagF(err, "Could not update authentication strength policy allowed combinations") + } + + } + + return authenticationStrengthPolicyRead(ctx, d, meta) + +} + +func authenticationStrengthPolicyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ConditionalAccess.AuthenticationStrengthPoliciesClient + + authenticationStrengthPolicy, status, err := client.Get(ctx, d.Id(), odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Authentication Strength Policy with Object ID %q was not found - removing from state", d.Id()) + d.SetId("") + return nil + } + } + if authenticationStrengthPolicy == nil { + return tf.ErrorDiagF(errors.New("Bad API response"), "Result is nil") + } + + d.SetId(*authenticationStrengthPolicy.ID) + tf.Set(d, "display_name", authenticationStrengthPolicy.DisplayName) + tf.Set(d, "description", authenticationStrengthPolicy.Description) + tf.Set(d, "allowed_combinations", tf.FlattenStringSlicePtr(authenticationStrengthPolicy.AllowedCombinations)) + + return nil +} + +func authenticationStrengthPolicyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ConditionalAccess.AuthenticationStrengthPoliciesClient + authenticationStrengthPolicyId := d.Id() + + if _, status, err := client.Get(ctx, authenticationStrengthPolicyId, odata.Query{}); err != nil { + if status == http.StatusNotFound { + log.Printf("[DEBUG] Authentication Strength Policy with ID %q already deleted", authenticationStrengthPolicyId) + return nil + } + + return tf.ErrorDiagPathF(err, "id", "Retrieving Authentication Strength Policy with ID %q", authenticationStrengthPolicyId) + } + + status, err := client.Delete(ctx, authenticationStrengthPolicyId) + if err != nil { + return tf.ErrorDiagPathF(err, "id", "Deleting Authentication Strength Policy with ID %q, got status %d", authenticationStrengthPolicyId, status) + } + + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + defer func() { client.BaseClient.DisableRetries = false }() + client.BaseClient.DisableRetries = true + if _, status, err := client.Get(ctx, authenticationStrengthPolicyId, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, err + } + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "waiting for deletion of Authentication Strength Policy with ID %q", authenticationStrengthPolicyId) + } + + return nil +} diff --git a/internal/services/conditionalaccess/authentication_strength_policy_resource_test.go b/internal/services/conditionalaccess/authentication_strength_policy_resource_test.go new file mode 100644 index 0000000000..c2e7405222 --- /dev/null +++ b/internal/services/conditionalaccess/authentication_strength_policy_resource_test.go @@ -0,0 +1,137 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package conditionalaccess_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/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/utils" +) + +type AuthenticationStrengthPolicyResource struct{} + +func TestAccAuthenticationStrengthPolicy_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_authentication_strength_policy", "test") + r := AuthenticationStrengthPolicyResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccAuthenticationStrengthPolicy_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_authentication_strength_policy", "test") + r := AuthenticationStrengthPolicyResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccAuthenticationStrengthPolicy_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_authentication_strength_policy", "test") + r := AuthenticationStrengthPolicyResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (r AuthenticationStrengthPolicyResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + var id *string + + authstrengthpolicy, status, err := clients.ConditionalAccess.AuthenticationStrengthPoliciesClient.Get(ctx, state.ID, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return nil, fmt.Errorf("Authentication Strength Policy with ID %q does not exist", state.ID) + } + return nil, fmt.Errorf("failed to retrieve Authentication Strength Policy with ID %q: %+v", state.ID, err) + } + id = authstrengthpolicy.ID + + return utils.Bool(id != nil && *id == state.ID), nil +} + +func (AuthenticationStrengthPolicyResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_authentication_strength_policy" "test" { + display_name = "acctestASP-%[1]d" + description = "test" + allowed_combinations = ["password"] +} +`, data.RandomInteger) +} + +func (AuthenticationStrengthPolicyResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_authentication_strength_policy" "test" { + display_name = "acctestASP-%[1]d" + description = "test" + allowed_combinations = [ + "fido2", + "password", + "deviceBasedPush", + "temporaryAccessPassOneTime", + "federatedMultiFactor", + "federatedSingleFactor", + "hardwareOath,federatedSingleFactor", + "microsoftAuthenticatorPush,federatedSingleFactor", + "password,hardwareOath", + "password,microsoftAuthenticatorPush", + "password,sms", + "password,softwareOath", + "password,voice", + "sms", + "sms,federatedSingleFactor", + "softwareOath,federatedSingleFactor", + "temporaryAccessPassMultiUse", + "voice,federatedSingleFactor", + "windowsHelloForBusiness", + "x509CertificateMultiFactor", + "x509CertificateSingleFactor", + ] +} +`, data.RandomInteger) +} diff --git a/internal/services/conditionalaccess/client/client.go b/internal/services/conditionalaccess/client/client.go index fa29ce21ea..c87004f262 100644 --- a/internal/services/conditionalaccess/client/client.go +++ b/internal/services/conditionalaccess/client/client.go @@ -9,8 +9,9 @@ import ( ) type Client struct { - NamedLocationsClient *msgraph.NamedLocationsClient - PoliciesClient *msgraph.ConditionalAccessPoliciesClient + NamedLocationsClient *msgraph.NamedLocationsClient + PoliciesClient *msgraph.ConditionalAccessPoliciesClient + AuthenticationStrengthPoliciesClient *msgraph.AuthenticationStrengthPoliciesClient } func NewClient(o *common.ClientOptions) *Client { @@ -20,8 +21,12 @@ func NewClient(o *common.ClientOptions) *Client { policiesClient := msgraph.NewConditionalAccessPoliciesClient() o.ConfigureClient(&policiesClient.BaseClient) + authenticationStrengthpoliciesClient := msgraph.NewAuthenticationStrengthPoliciesClient() + o.ConfigureClient(&authenticationStrengthpoliciesClient.BaseClient) + return &Client{ - NamedLocationsClient: namedLocationsClient, - PoliciesClient: policiesClient, + NamedLocationsClient: namedLocationsClient, + PoliciesClient: policiesClient, + AuthenticationStrengthPoliciesClient: authenticationStrengthpoliciesClient, } } diff --git a/internal/services/conditionalaccess/conditional_access_policy_resource.go b/internal/services/conditionalaccess/conditional_access_policy_resource.go index 33359a5410..2b12744e08 100644 --- a/internal/services/conditionalaccess/conditional_access_policy_resource.go +++ b/internal/services/conditionalaccess/conditional_access_policy_resource.go @@ -387,8 +387,9 @@ func conditionalAccessPolicyResource() *schema.Resource { }, "built_in_controls": { - Type: schema.TypeList, - Required: true, + Type: schema.TypeList, + Optional: true, + AtLeastOneOf: []string{"grant_controls.0.built_in_controls", "grant_controls.0.authentication_strength_id", "grant_controls.0.terms_of_use"}, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.StringInSlice([]string{ @@ -404,6 +405,13 @@ func conditionalAccessPolicyResource() *schema.Resource { }, }, + "authentication_strength_id": { + AtLeastOneOf: []string{"grant_controls.0.built_in_controls", "grant_controls.0.authentication_strength_id", "grant_controls.0.terms_of_use"}, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + }, + "custom_authentication_factors": { Type: schema.TypeList, Optional: true, @@ -414,8 +422,9 @@ func conditionalAccessPolicyResource() *schema.Resource { }, "terms_of_use": { - Type: schema.TypeList, - Optional: true, + Type: schema.TypeList, + Optional: true, + AtLeastOneOf: []string{"grant_controls.0.built_in_controls", "grant_controls.0.authentication_strength_id", "grant_controls.0.terms_of_use"}, Elem: &schema.Schema{ Type: schema.TypeString, ValidateDiagFunc: validate.NoEmptyStrings, diff --git a/internal/services/conditionalaccess/conditionalaccess.go b/internal/services/conditionalaccess/conditionalaccess.go index 0c08557abe..b6d60ee0b8 100644 --- a/internal/services/conditionalaccess/conditionalaccess.go +++ b/internal/services/conditionalaccess/conditionalaccess.go @@ -121,6 +121,7 @@ func flattenConditionalAccessGrantControls(in *msgraph.ConditionalAccessGrantCon map[string]interface{}{ "operator": in.Operator, "built_in_controls": tf.FlattenStringSlicePtr(in.BuiltInControls), + "authentication_strength_id": in.AuthenticationStrength.ID, "custom_authentication_factors": tf.FlattenStringSlicePtr(in.CustomAuthenticationFactors), "terms_of_use": tf.FlattenStringSlicePtr(in.TermsOfUse), }, @@ -392,11 +393,15 @@ func expandConditionalAccessGrantControls(in []interface{}) *msgraph.Conditional config := in[0].(map[string]interface{}) operator := config["operator"].(string) + authenticationStrengthId := config["authentication_strength_id"].(string) builtInControls := config["built_in_controls"].([]interface{}) customAuthenticationFactors := config["custom_authentication_factors"].([]interface{}) termsOfUse := config["terms_of_use"].([]interface{}) result.Operator = &operator + result.AuthenticationStrength = &msgraph.AuthenticationStrengthPolicy{ + ID: &authenticationStrengthId, + } result.BuiltInControls = tf.ExpandStringSlicePtr(builtInControls) result.CustomAuthenticationFactors = tf.ExpandStringSlicePtr(customAuthenticationFactors) result.TermsOfUse = tf.ExpandStringSlicePtr(termsOfUse) diff --git a/internal/services/conditionalaccess/registration.go b/internal/services/conditionalaccess/registration.go index 6b35e66e6e..beeb403263 100644 --- a/internal/services/conditionalaccess/registration.go +++ b/internal/services/conditionalaccess/registration.go @@ -31,7 +31,8 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { // SupportedResources returns the supported Resources supported by this Service func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "azuread_named_location": namedLocationResource(), - "azuread_conditional_access_policy": conditionalAccessPolicyResource(), + "azuread_named_location": namedLocationResource(), + "azuread_conditional_access_policy": conditionalAccessPolicyResource(), + "azuread_authentication_strength_policy": authenticationStrengthPolicyResource(), } } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/authentication_strength_policy.go b/vendor/github.com/manicminer/hamilton/msgraph/authentication_strength_policy.go index 25efbe5e89..43d964d620 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/authentication_strength_policy.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/authentication_strength_policy.go @@ -141,6 +141,37 @@ func (c *AuthenticationStrengthPoliciesClient) Update(ctx context.Context, Authe return status, nil } +func (c *AuthenticationStrengthPoliciesClient) UpdateAllowedCombinations(ctx context.Context, Policy AuthenticationStrengthPolicy) (int, error) { + var status int + + if Policy.ID == nil { + return status, errors.New("cannot update AuthenticationStrengthPolicy with nil ID") + } + + if Policy.AllowedCombinations == nil { + return status, errors.New("cannot update AuthenticationStrengthPolicy with nil AllowedCombinations") + } + + body, err := json.Marshal(Policy) + if err != nil { + return status, fmt.Errorf("json.Marshal(): %v", err) + } + + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/policies/authenticationStrengthPolicies/%s/updateAllowedCombinations", *Policy.ID), + }, + }) + if err != nil { + return status, fmt.Errorf("AuthenticationStrengthPoliciesClient.BaseClient.Post(): %v", err) + } + + return status, nil +} + // Delete removes a AuthenticationStrengthPolicy. func (c *AuthenticationStrengthPoliciesClient) Delete(ctx context.Context, id string) (int, error) { _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{