Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add support for userflow attribute assignment #899

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
14 changes: 14 additions & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@ TEST?=$$(go list ./... |grep -v 'vendor')
PKG_NAME=internal
PROVIDER=azuread

# Values to install the provider locally for testing purposes
HOSTNAME=registry.terraform.io
NAMESPACE=hashicorp
NAME=azuread
BINARY=terraform-provider-${NAME}
VERSION=9.9.9
OS_ARCH=linux_amd64

.EXPORT_ALL_VARIABLES:
TF_SCHEMA_PANIC_ON_ERROR=1
GO111MODULE=on

default: build

local-build:
go build -o ${BINARY}

local-install: local-build
mkdir -p ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH}
mv ${BINARY} ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH}

tools:
@echo "==> installing required tooling..."
@sh "$(CURDIR)/scripts/gogetcookie.sh"
Expand Down
3 changes: 3 additions & 0 deletions internal/clients/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
administrativeunits "github.com/hashicorp/terraform-provider-azuread/internal/services/administrativeunits/client"
applications "github.com/hashicorp/terraform-provider-azuread/internal/services/applications/client"
approleassignments "github.com/hashicorp/terraform-provider-azuread/internal/services/approleassignments/client"
userflow "github.com/hashicorp/terraform-provider-azuread/internal/services/b2cuserflow/client"
conditionalaccess "github.com/hashicorp/terraform-provider-azuread/internal/services/conditionalaccess/client"
directoryroles "github.com/hashicorp/terraform-provider-azuread/internal/services/directoryroles/client"
domains "github.com/hashicorp/terraform-provider-azuread/internal/services/domains/client"
Expand All @@ -37,6 +38,7 @@ type Client struct {
AdministrativeUnits *administrativeunits.Client
Applications *applications.Client
AppRoleAssignments *approleassignments.Client
B2CUserFlow *userflow.Client
ConditionalAccess *conditionalaccess.Client
DirectoryRoles *directoryroles.Client
Domains *domains.Client
Expand All @@ -61,6 +63,7 @@ func (client *Client) build(ctx context.Context, o *common.ClientOptions) error
client.Policies = policies.NewClient(o)
client.ServicePrincipals = serviceprincipals.NewClient(o)
client.Users = users.NewClient(o)
client.B2CUserFlow = userflow.NewClient(o)

// Acquire an access token upfront, so we can decode the JWT and populate the claims
token, err := o.Authorizer.Token()
Expand Down
4 changes: 4 additions & 0 deletions internal/provider/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/hashicorp/terraform-provider-azuread/internal/services/administrativeunits"
"github.com/hashicorp/terraform-provider-azuread/internal/services/applications"
"github.com/hashicorp/terraform-provider-azuread/internal/services/approleassignments"
"github.com/hashicorp/terraform-provider-azuread/internal/services/b2cuserflow"
"github.com/hashicorp/terraform-provider-azuread/internal/services/conditionalaccess"
"github.com/hashicorp/terraform-provider-azuread/internal/services/directoryobjects"
"github.com/hashicorp/terraform-provider-azuread/internal/services/directoryroles"
Expand All @@ -12,6 +13,7 @@ import (
"github.com/hashicorp/terraform-provider-azuread/internal/services/invitations"
"github.com/hashicorp/terraform-provider-azuread/internal/services/policies"
"github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals"
"github.com/hashicorp/terraform-provider-azuread/internal/services/userflowattributeassignment"
"github.com/hashicorp/terraform-provider-azuread/internal/services/users"
)

Expand All @@ -20,6 +22,7 @@ func SupportedServices() []ServiceRegistration {
administrativeunits.Registration{},
applications.Registration{},
approleassignments.Registration{},
b2cuserflow.Registration{},
conditionalaccess.Registration{},
directoryobjects.Registration{},
directoryroles.Registration{},
Expand All @@ -28,6 +31,7 @@ func SupportedServices() []ServiceRegistration {
invitations.Registration{},
policies.Registration{},
serviceprincipals.Registration{},
userflowattributeassignment.Registration{},
users.Registration{},
}
}
20 changes: 20 additions & 0 deletions internal/services/b2cuserflow/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package b2cuserflow

import (
"github.com/manicminer/hamilton/msgraph"

"github.com/hashicorp/terraform-provider-azuread/internal/common"
)

type Client struct {
UserFlowClient *msgraph.B2CUserFlowClient
}

func NewClient(o *common.ClientOptions) *Client {
userFlowClient := msgraph.NewB2CUserFlowClient(o.TenantID)
o.ConfigureClient(&userFlowClient.BaseClient)

return &Client{
UserFlowClient: userFlowClient,
}
}
31 changes: 31 additions & 0 deletions internal/services/b2cuserflow/registration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package b2cuserflow

import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

type Registration struct{}

// Name is the name of this Service
func (r Registration) Name() string {
return "B2CUserFlow"
}

// WebsiteCategories returns a list of categories which can be used for the sidebar
func (r Registration) WebsiteCategories() []string {
return []string{
"B2CUserFlow",
}
}

// SupportedDataSources returns the supported Data Sources supported by this Service
func (r Registration) SupportedDataSources() map[string]*schema.Resource {
return nil
}

// SupportedResources returns the supported Resources supported by this Service
func (r Registration) SupportedResources() map[string]*schema.Resource {
return map[string]*schema.Resource{
"azuread_b2c_userflow": b2cUserflowResource(),
}
}
200 changes: 200 additions & 0 deletions internal/services/b2cuserflow/userflow_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package b2cuserflow

import (
"context"
"errors"
"fmt"
"log"
"net/http"
"time"

"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-plugin-sdk/v2/helper/validation"
"github.com/manicminer/hamilton/msgraph"
"github.com/manicminer/hamilton/odata"

"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"
)

func b2cUserflowResource() *schema.Resource {
return &schema.Resource{
CreateContext: b2cuserflowResourceCreate,
ReadContext: b2cuserflowResourceRead,
UpdateContext: b2cuserflowResourceUpdate,
DeleteContext: b2cuserflowResourceDelete,

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{
"object_id": {
Description: "The object ID of the userflow",
Type: schema.TypeString,
Computed: true,
},
"name": {
Description: "The name of the user flow. This is a required value and is immutable after it's created. The name will be prefixed with the value of B2C_1_ after creation.",
Type: schema.TypeString,
Required: true,
ValidateDiagFunc: validate.NoEmptyStrings,
},
"user_flow_type": {
Description: "The type of user flow. The supported values for userFlowType are: signUp, signIn, signUpOrSignIn, passwordReset, profileUpdate, resourceOwner.",
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
string("signUp"),
string("signIn"),
string("signUpOrSignIn"),
string("passwordReset"),
string("profileUpdate"),
string("resourceOwner"),
}, false),
},

"user_flow_type_version": {
Description: "The version of the user flow",
Type: schema.TypeFloat,
Required: true,
},
"default_language_tag": {
Description: "Indicates the default language of the b2cIdentityUserFlow that is used when no ui_locale tag is specified in the request. This field is RFC 5646 compliant.",
Type: schema.TypeString,
Optional: true,
},
"is_language_customization_enabled": {
Description: " The property that determines whether language customization is enabled within the B2C user flow. Language customization is not enabled by default for B2C user flows.",
Type: schema.TypeBool,
Optional: true,
},
},
}
}

func b2cuserflowResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).B2CUserFlow.UserFlowClient
name := d.Get("name").(string)
userflowType := d.Get("user_flow_type").(string)
userflowTypeVersion := float32(d.Get("user_flow_type_version").(float64))
defaultTag := d.Get("default_language_tag").(string)
isLanguageCustomizationEnabled := d.Get("is_language_customization_enabled").(bool)
userflow := msgraph.B2CUserFlow{
ID: &name,
UserFlowType: &userflowType,
UserFlowTypeVersion: &userflowTypeVersion,
DefaultLanguageTag: &defaultTag,
IsLanguageCustomizationEnabled: &isLanguageCustomizationEnabled,
}
userflowResp, _, err := client.Create(ctx, userflow)
if err != nil {
return tf.ErrorDiagF(err, "Creating userflow %+v", userflow)
}

if userflowResp.ID == nil || *userflowResp.ID == "" {
return tf.ErrorDiagF(errors.New("API returned nil object ID"), "Bad API Response")
}

d.SetId(fmt.Sprintf("B2C_1_%s", name))
return b2cuserflowResourceRead(ctx, d, meta)
}

func b2cuserflowResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
if d.HasChange("user_flow_type") {
return tf.ErrorDiagF(errors.New("Cannot update user_flow_type"), "Cannot update user_flow_type")
}
if d.HasChange("user_flow_type_version") {
return tf.ErrorDiagF(errors.New("Cannot update user_flow_type_version"), "Cannot update user_flow_type_version")
}

if d.HasChange("name") {
return tf.ErrorDiagF(errors.New("Cannot update name"), "Cannot update name")
}
objectId := d.Id()
defaultTag := d.Get("default_language_tag").(string)
isLanguageCustomizationEnabled := d.Get("is_language_customization_enabled").(bool)

userflow := msgraph.B2CUserFlow{
ID: &objectId,
DefaultLanguageTag: &defaultTag,
IsLanguageCustomizationEnabled: &isLanguageCustomizationEnabled,
}

client := meta.(*clients.Client).B2CUserFlow.UserFlowClient
_, err := client.Update(ctx, userflow)
if err != nil {
return tf.ErrorDiagF(err, "Could not update userflow with ID: %q", d.Id())
}
return b2cuserflowResourceRead(ctx, d, meta)
}

func b2cuserflowResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).B2CUserFlow.UserFlowClient

objectId := d.Id()

userflow, status, err := client.Get(ctx, objectId, odata.Query{})
if err != nil {
if status == http.StatusNotFound {
log.Printf("[DEBUG] Userflow with Object ID %q was not found - removing from state!", objectId)
d.SetId("")
return nil
}
return tf.ErrorDiagF(err, "Retrieving userflow with object ID: %q", objectId)
}

tf.Set(d, "object_id", *userflow.ID)
tf.Set(d, "user_flow_type", *userflow.UserFlowType)
tf.Set(d, "user_flow_type_version", *userflow.UserFlowTypeVersion)
tf.Set(d, "default_language_tag", *userflow.DefaultLanguageTag)
tf.Set(d, "is_language_customization_enabled", *userflow.IsLanguageCustomizationEnabled)
return nil
}

func b2cuserflowResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).B2CUserFlow.UserFlowClient

objectId := d.Id()

status, err := client.Delete(ctx, objectId)
if err != nil {
if status == http.StatusNotFound {
log.Printf("[DEBUG] Userflow with Object ID %q was not found - removing from state!", objectId)
d.SetId("")
return nil
}
return tf.ErrorDiagPathF(err, "id", "Deleting userflow with object ID %q, got status %d", objectId, status)
}

// Wait for userflow object to be deleted
if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) {
client.BaseClient.DisableRetries = true
if _, status, err := client.Get(ctx, objectId, 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 userflow with object ID %q", objectId)
}

return nil
}
Loading