diff --git a/internal/test/testing.go b/internal/test/testing.go index 2ba38e7a..b0359a37 100644 --- a/internal/test/testing.go +++ b/internal/test/testing.go @@ -101,6 +101,7 @@ type Test struct { ApplicationsClient *msgraph.ApplicationsClient AppRoleAssignedToClient *msgraph.AppRoleAssignedToClient AuthenticationMethodsClient *msgraph.AuthenticationMethodsClient + B2CUserFlowClient *msgraph.B2CUserFlowClient ClaimsMappingPolicyClient *msgraph.ClaimsMappingPolicyClient ConditionalAccessPoliciesClient *msgraph.ConditionalAccessPoliciesClient DelegatedPermissionGrantsClient *msgraph.DelegatedPermissionGrantsClient @@ -216,6 +217,11 @@ func NewTest(t *testing.T) (c *Test) { c.AuthenticationMethodsClient.BaseClient.Endpoint = c.Connection.AuthConfig.Environment.MsGraph.Endpoint c.AuthenticationMethodsClient.BaseClient.RetryableClient.RetryMax = retry + c.B2CUserFlowClient = msgraph.NewB2CUserFlowClient(c.Connection.AuthConfig.TenantID) + c.B2CUserFlowClient.BaseClient.Authorizer = c.Connection.Authorizer + c.B2CUserFlowClient.BaseClient.Endpoint = c.Connection.AuthConfig.Environment.MsGraph.Endpoint + c.B2CUserFlowClient.BaseClient.RetryableClient.RetryMax = retry + c.ClaimsMappingPolicyClient = msgraph.NewClaimsMappingPolicyClient(c.Connection.AuthConfig.TenantID) c.ClaimsMappingPolicyClient.BaseClient.Authorizer = c.Connection.Authorizer c.ClaimsMappingPolicyClient.BaseClient.Endpoint = c.Connection.AuthConfig.Environment.MsGraph.Endpoint diff --git a/internal/utils/pointers.go b/internal/utils/pointers.go index fc780207..967186d4 100644 --- a/internal/utils/pointers.go +++ b/internal/utils/pointers.go @@ -24,3 +24,8 @@ func StringPtr(s string) *string { func ArrayStringPtr(s []string) *[]string { return &s } + +// Float32Ptr returns a pointer to the provided float32 variable. +func Float32Ptr(f float32) *float32 { + return &f +} diff --git a/msgraph/b2c_userflow.go b/msgraph/b2c_userflow.go new file mode 100644 index 00000000..a37e0d92 --- /dev/null +++ b/msgraph/b2c_userflow.go @@ -0,0 +1,171 @@ +package msgraph + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/manicminer/hamilton/odata" +) + +// B2CUserFlowClient performs operations on B2CUserFlow. +type B2CUserFlowClient struct { + BaseClient Client +} + +// NewB2CUserFlowClient returns a new B2CUserFlowClient. +func NewB2CUserFlowClient(tenantId string) *B2CUserFlowClient { + return &B2CUserFlowClient{ + BaseClient: NewClient(VersionBeta, tenantId), + } +} + +// List returns a list of B2C UserFlows, optionally queried using OData. +func (c *B2CUserFlowClient) List(ctx context.Context, query odata.Query) (*[]B2CUserFlow, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + OData: query, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/identity/b2cUserFlows", + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("B2CUserFlowClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + UserFlows []B2CUserFlow `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.UserFlows, status, nil +} + +// Create creates a new B2CUserFlow. +func (c *B2CUserFlowClient) Create(ctx context.Context, userflow B2CUserFlow) (*B2CUserFlow, int, error) { + var status int + + body, err := json.Marshal(userflow) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + OData: odata.Query{ + Metadata: odata.MetadataFull, + }, + ValidStatusCodes: []int{http.StatusCreated}, + Uri: Uri{ + Entity: "/identity/b2cUserFlows", + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("B2CUserFlowClient.BaseClient.Post(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var newUserFlow B2CUserFlow + if err := json.Unmarshal(respBody, &newUserFlow); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &newUserFlow, status, nil +} + +// Get returns an existing B2CUserFlow. +func (c *B2CUserFlowClient) Get(ctx context.Context, id string, query odata.Query) (*B2CUserFlow, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + OData: query, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/identity/b2cUserFlows/%s", id), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("B2CUserFlowClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var userflow B2CUserFlow + if err := json.Unmarshal(respBody, &userflow); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &userflow, status, nil +} + +// Update amends an existing B2CUserFlow. +func (c *B2CUserFlowClient) Update(ctx context.Context, userflow B2CUserFlow) (int, error) { + var status int + if userflow.ID == nil { + return status, fmt.Errorf("cannot update userflow with nil ID") + } + + userflowID := *userflow.ID + userflow.ID = nil + + body, err := json.Marshal(userflow) + if err != nil { + return status, fmt.Errorf("json.Marshal(): %v", err) + } + + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{ + http.StatusOK, + http.StatusNoContent, + }, + Uri: Uri{ + Entity: fmt.Sprintf("/identity/b2cUserFlows//%s", userflowID), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("B2CUserFlowClient.BaseClient.Patch(): %v", err) + } + + return status, nil +} + +// Delete removes a B2CUserFlow. +func (c *B2CUserFlowClient) Delete(ctx context.Context, id string) (int, error) { + _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/identity/b2cUserFlows/%s", id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("B2CUserFlowClient.BaseClient.Delete(): %v", err) + } + + return status, nil +} diff --git a/msgraph/b2c_userflow_test.go b/msgraph/b2c_userflow_test.go new file mode 100644 index 00000000..e0b34a39 --- /dev/null +++ b/msgraph/b2c_userflow_test.go @@ -0,0 +1,78 @@ +package msgraph_test + +import ( + "testing" + + "github.com/manicminer/hamilton/internal/test" + "github.com/manicminer/hamilton/internal/utils" + "github.com/manicminer/hamilton/msgraph" + "github.com/manicminer/hamilton/odata" +) + +func TestB2CUserFlowClient(t *testing.T) { + c := test.NewTest(t) + defer c.CancelFunc() + + userflow := testB2CUserFlowClient_Create(t, c, msgraph.B2CUserFlow{ + ID: utils.StringPtr("test b2c user flow"), + UserFlowType: utils.StringPtr("signup"), + UserFlowTypeVersion: utils.Float32Ptr(3.0), + }) + testB2CUserFlowClient_Get(t, c, *userflow.ID) + userflow.DefaultLanguageTag = utils.StringPtr("en") + testB2CUserFlowClient_Update(t, c, *userflow) + testB2CUserFlowClient_List(t, c) + testGroupsClient_Delete(t, c, *userflow.ID) +} + +func testB2CUserFlowClient_Create(t *testing.T, c *test.Test, u msgraph.B2CUserFlow) *msgraph.B2CUserFlow { + userflow, status, err := c.B2CUserFlowClient.Create(c.Context, u) + if err != nil { + t.Fatalf("B2CUserFlowclient.Create(): %v", err) + } + if status < 200 || status >= 300 { + t.Fatalf("B2CUserFlowClient.Create(): invalid status: %d", status) + } + if userflow == nil { + t.Fatal("B2CUserFlowClient.Create(): userflow was nil") + } + if userflow.ID == nil { + t.Fatal("B2CUserFlowClient.Create(): userflow.ID was nil") + } + return userflow +} + +func testB2CUserFlowClient_Get(t *testing.T, c *test.Test, id string) *msgraph.B2CUserFlow { + userflow, status, err := c.B2CUserFlowClient.Get(c.Context, id, odata.Query{}) + if err != nil { + t.Fatalf("B2CUserFlowClient.Get(): %v", err) + } + if status < 200 || status >= 300 { + t.Fatalf("B2CUserFlowClient.Get(): invalid status: %d", status) + } + if userflow == nil { + t.Fatal("B2CUserFlowClient.Get(): userflow was nil") + } + return userflow +} + +func testB2CUserFlowClient_List(t *testing.T, c *test.Test) *[]msgraph.B2CUserFlow { + userflows, _, err := c.B2CUserFlowClient.List(c.Context, odata.Query{Top: 10}) + if err != nil { + t.Fatalf("B2CUserFlowClient.List(): %v", err) + } + if userflows == nil { + t.Fatal("B2CUserFlowClient.List(): userflows was nil") + } + return userflows +} + +func testB2CUserFlowClient_Update(t *testing.T, c *test.Test, u msgraph.B2CUserFlow) { + status, err := c.B2CUserFlowClient.Update(c.Context, u) + if err != nil { + t.Fatalf("B2CUserFlowClient.Update(): %v", err) + } + if status < 200 || status >= 300 { + t.Fatalf("B2CUserFlowClient.Update(): invalid status: %d", status) + } +} diff --git a/msgraph/models.go b/msgraph/models.go index 6b83c553..70f3266d 100644 --- a/msgraph/models.go +++ b/msgraph/models.go @@ -1688,6 +1688,16 @@ type EmployeeOrgData struct { Division *string `json:"division,omitempty"` } +type B2CUserFlow struct { + ID *string `json:"id,omitempty"` + UserFlowType *string `json:"userFlowType,omitempty"` + UserFlowTypeVersion *float32 `json:"userFlowTypeVersion,omitempty"` + // 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. + IsLanguageCustomizationEnabled *bool `json:"IsLanguageCustomizationEnabled,omitempty"` + // 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. + DefaultLanguageTag *string `json:"defaultLanguageTag,omitempty"` +} + type UserFlowAttribute struct { ID *string `json:"id,omitempty"` Description *string `json:"description,omitempty"`