From 156d0139cac1d8bde5311e48159b35d95d0e086e Mon Sep 17 00:00:00 2001 From: Doron Sharon Date: Wed, 1 Nov 2023 15:16:06 +0200 Subject: [PATCH 1/2] Add the ability to specify invite type (#324) --- descope/internal/mgmt/user.go | 10 +++++++++- descope/internal/mgmt/user_test.go | 16 +++++++++++++++- descope/types.go | 2 ++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/descope/internal/mgmt/user.go b/descope/internal/mgmt/user.go index 80d8acb2..dfd73f79 100644 --- a/descope/internal/mgmt/user.go +++ b/descope/internal/mgmt/user.go @@ -416,7 +416,15 @@ func makeCreateUserRequest(loginID, email, phone, displayName, picture string, r req["test"] = true } if options != nil { - req["inviteUrl"] = options.InviteURL + if len(options.InviteURL) > 0 { + req["inviteUrl"] = options.InviteURL + } + if options.SendMail != nil { + req["sendMail"] = *options.SendMail + } + if options.SendSMS != nil { + req["sendSMS"] = *options.SendSMS + } } return req } diff --git a/descope/internal/mgmt/user_test.go b/descope/internal/mgmt/user_test.go index 639690cb..db39eff2 100644 --- a/descope/internal/mgmt/user_test.go +++ b/descope/internal/mgmt/user_test.go @@ -16,6 +16,7 @@ func TestUserCreateSuccess(t *testing.T) { "email": "a@b.c", }} ca := map[string]any{"ak": "av"} + i := 0 m := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) { require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key") req := map[string]any{} @@ -27,6 +28,15 @@ func TestUserCreateSuccess(t *testing.T) { require.Equal(t, "foo", roleNames[0]) require.Nil(t, req["test"]) assert.EqualValues(t, ca, req["customAttributes"]) + + if i == 2 { + assert.True(t, true, req["sendSMS"]) + assert.EqualValues(t, false, req["sendMail"]) + } else { + assert.Nil(t, req["sendSMS"]) + assert.Nil(t, req["sendMail"]) + } + i++ }, response)) user := &descope.UserRequest{} user.Email = "foo@bar.com" @@ -42,7 +52,9 @@ func TestUserCreateSuccess(t *testing.T) { require.NotNil(t, res) require.Equal(t, "a@b.c", res.Email) - res, err = m.User().Invite("abc", user, &descope.InviteOptions{InviteURL: "https://some.domain.com"}) + sendSMS := true + sendMail := false + res, err = m.User().Invite("abc", user, &descope.InviteOptions{InviteURL: "https://some.domain.com", SendSMS: &sendSMS, SendMail: &sendMail}) require.NoError(t, err) require.NotNil(t, res) require.Equal(t, "a@b.c", res.Email) @@ -66,6 +78,8 @@ func TestUserCreateSuccessWithOptions(t *testing.T) { require.Nil(t, req["test"]) assert.EqualValues(t, ca, req["customAttributes"]) assert.EqualValues(t, "https://some.domain.com", req["inviteUrl"]) + assert.Nil(t, req["sendMail"]) + assert.Nil(t, req["sendSMS"]) }, response)) user := &descope.UserRequest{} user.Email = "foo@bar.com" diff --git a/descope/types.go b/descope/types.go index cf34b788..40880f25 100644 --- a/descope/types.go +++ b/descope/types.go @@ -262,6 +262,8 @@ func NewToken(JWT string, token jwt.Token) *Token { type InviteOptions struct { InviteURL string `json:"inviteUrl,omitempty"` + SendMail *bool `json:"sendMail,omitempty"` // send invite via mail, default is according to project settings + SendSMS *bool `json:"sendSMS,omitempty"` // send invite via text message, default is according to project settings } type User struct { From 6f04bc78f595c693d080df2d749474e618903d84 Mon Sep 17 00:00:00 2001 From: Doron Sharon Date: Thu, 2 Nov 2023 14:18:37 +0200 Subject: [PATCH 2/2] Add support for batch invite (#325) --- README.md | 16 +++++ descope/api/client.go | 6 ++ descope/internal/mgmt/user.go | 49 ++++++++++++++ descope/internal/mgmt/user_test.go | 75 ++++++++++++++++++++++ descope/sdk/mgmt.go | 17 ++++- descope/tests/mocks/mgmt/managementmock.go | 11 ++++ descope/types.go | 15 +++++ 7 files changed, 186 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2bd35872..8e0416bf 100644 --- a/README.md +++ b/README.md @@ -590,6 +590,22 @@ userReqInvite.Tenants = []*descope.AssociatedTenant{ options := &descope.InviteOptions{InviteURL: "https://sub.domain.com"} err := descopeClient.Management.User().Invite("desmond@descope.com", userReqInvite, options) +// batch invite +options := &descope.InviteOptions{InviteURL: "https://sub.domain.com"} +batchUsers := []*descope.BatchUser{} +u1 := &descope.BatchUser{} +u1.LoginID = "one" +u1.Email = "one@one.com" +u1.Roles = []string{"one"} + +u2 := &descope.BatchUser{} +u2.LoginID = "two" +u2.Email = "two@two.com" +u2.Roles = []string{"two"} + +batchUsers = append(batchUsers, u1, u2) +err := descopeClient.Management.User().InviteBatch(batchUsers, options) + // Update will override all fields as is. Use carefully. userReqUpdate := &descope.UserRequest{} userReqUpdate.Email = "desmond@descope.com" diff --git a/descope/api/client.go b/descope/api/client.go index b7c6b78f..3188b375 100644 --- a/descope/api/client.go +++ b/descope/api/client.go @@ -79,6 +79,7 @@ var ( tenantLoadAll: "mgmt/tenant/all", tenantSearchAll: "mgmt/tenant/search", userCreate: "mgmt/user/create", + userCreateBatch: "mgmt/user/create/batch", userUpdate: "mgmt/user/update", userDelete: "mgmt/user/delete", userDeleteAllTestUsers: "mgmt/user/test/delete/all", @@ -205,6 +206,7 @@ type mgmtEndpoints struct { tenantSearchAll string userCreate string + userCreateBatch string userUpdate string userDelete string userDeleteAllTestUsers string @@ -437,6 +439,10 @@ func (e *endpoints) ManagementUserCreate() string { return path.Join(e.version, e.mgmt.userCreate) } +func (e *endpoints) ManagementUserCreateBatch() string { + return path.Join(e.version, e.mgmt.userCreateBatch) +} + func (e *endpoints) ManagementUserUpdate() string { return path.Join(e.version, e.mgmt.userUpdate) } diff --git a/descope/internal/mgmt/user.go b/descope/internal/mgmt/user.go index dfd73f79..4976ef6d 100644 --- a/descope/internal/mgmt/user.go +++ b/descope/internal/mgmt/user.go @@ -31,6 +31,13 @@ func (u *user) Invite(loginID string, user *descope.UserRequest, options *descop return u.create(loginID, user.Email, user.Phone, user.Name, user.Picture, user.Roles, user.Tenants, true, false, user.CustomAttributes, user.VerifiedEmail, user.VerifiedPhone, options) } +func (u *user) InviteBatch(users []*descope.BatchUser, options *descope.InviteOptions) (*descope.UsersBatchResponse, error) { + if users == nil { + users = []*descope.BatchUser{} + } + return u.createBatch(users, options) +} + func (u *user) create(loginID, email, phone, displayName, picture string, roles []string, tenants []*descope.AssociatedTenant, invite, test bool, customAttributes map[string]any, verifiedEmail *bool, verifiedPhone *bool, options *descope.InviteOptions) (*descope.UserResponse, error) { if loginID == "" { return nil, utils.NewInvalidArgumentError("loginID") @@ -43,6 +50,15 @@ func (u *user) create(loginID, email, phone, displayName, picture string, roles return unmarshalUserResponse(res) } +func (u *user) createBatch(users []*descope.BatchUser, options *descope.InviteOptions) (*descope.UsersBatchResponse, error) { + req := makeCreateUsersBatchRequest(users, options) + res, err := u.client.DoPostRequest(api.Routes.ManagementUserCreateBatch(), req, nil, u.conf.ManagementKey) + if err != nil { + return nil, err + } + return unmarshalUserBatchResponse(res) +} + func (u *user) Update(loginID string, user *descope.UserRequest) (*descope.UserResponse, error) { if loginID == "" { return nil, utils.NewInvalidArgumentError("loginID") @@ -429,6 +445,30 @@ func makeCreateUserRequest(loginID, email, phone, displayName, picture string, r return req } +func makeCreateUsersBatchRequest(users []*descope.BatchUser, options *descope.InviteOptions) map[string]any { + var usersReq []map[string]any + for _, u := range users { + usersReq = append(usersReq, makeUpdateUserRequest(u.LoginID, u.Email, u.Phone, u.Name, u.Picture, u.Roles, u.Tenants, u.CustomAttributes, u.VerifiedEmail, u.VerifiedPhone)) + } + req := map[string]any{ + "users": usersReq, + } + if options != nil { + req["invite"] = true + if len(options.InviteURL) > 0 { + req["inviteUrl"] = options.InviteURL + } + if options.SendMail != nil { + req["sendMail"] = *options.SendMail + } + if options.SendSMS != nil { + req["sendSMS"] = *options.SendSMS + } + } + + return req +} + func makeUpdateUserRequest(loginID, email, phone, displayName, picture string, roles []string, tenants []*descope.AssociatedTenant, customAttributes map[string]any, verifiedEmail *bool, verifiedPhone *bool) map[string]any { res := map[string]any{ "loginId": loginID, @@ -503,6 +543,15 @@ func unmarshalUserResponse(res *api.HTTPResponse) (*descope.UserResponse, error) return ures.User, err } +func unmarshalUserBatchResponse(res *api.HTTPResponse) (*descope.UsersBatchResponse, error) { + ures := &descope.UsersBatchResponse{} + err := utils.Unmarshal([]byte(res.BodyStr), ures) + if err != nil { + return nil, err + } + return ures, err +} + func unmarshalUserSearchAllResponse(res *api.HTTPResponse) ([]*descope.UserResponse, error) { ures := struct { Users []*descope.UserResponse diff --git a/descope/internal/mgmt/user_test.go b/descope/internal/mgmt/user_test.go index db39eff2..0cc17604 100644 --- a/descope/internal/mgmt/user_test.go +++ b/descope/internal/mgmt/user_test.go @@ -92,6 +92,81 @@ func TestUserCreateSuccessWithOptions(t *testing.T) { require.Equal(t, "a@b.c", res.Email) } +func TestUsersInviteBatchSuccess(t *testing.T) { + response := map[string]any{ + "createdUsers": []map[string]any{ + {"email": "one@one.com"}, + }, + "failedUsers": []map[string]any{ + { + "user": map[string]any{ + "email": "two@two.com", + }, + "failure": "some failure", + }, + }, + } + ca := map[string]any{"ak": "av"} + + users := []*descope.BatchUser{} + + u1 := &descope.BatchUser{} + u1.LoginID = "one" + u1.Email = "one@one.com" + u1.Roles = []string{"one"} + u1.CustomAttributes = ca + + u2 := &descope.BatchUser{} + u2.LoginID = "two" + u2.Email = "two@two.com" + u2.Roles = []string{"two"} + + users = append(users, u1, u2) + + sendSMS := true + + called := false + m := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) { + called = true + require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key") + req := map[string]any{} + require.NoError(t, helpers.ReadBody(r, &req)) + assert.EqualValues(t, true, req["invite"]) + assert.EqualValues(t, "https://some.domain.com", req["inviteUrl"]) + assert.Nil(t, req["sendMail"]) + assert.EqualValues(t, true, req["sendSMS"]) + usersRes := req["users"].([]any) + userRes1 := usersRes[0].(map[string]any) + userRes2 := usersRes[1].(map[string]any) + require.Equal(t, u1.LoginID, userRes1["loginId"]) + require.Equal(t, u1.Email, userRes1["email"]) + assert.EqualValues(t, ca, userRes1["customAttributes"]) + roleNames := userRes1["roleNames"].([]any) + require.Len(t, roleNames, 1) + require.Equal(t, u1.Roles[0], roleNames[0]) + + require.Equal(t, u2.LoginID, userRes2["loginId"]) + require.Equal(t, u2.Email, userRes2["email"]) + assert.Nil(t, userRes2["customAttributes"]) + roleNames = userRes2["roleNames"].([]any) + require.Len(t, roleNames, 1) + require.Equal(t, u2.Roles[0], roleNames[0]) + }, response)) + + res, err := m.User().InviteBatch(users, &descope.InviteOptions{ + InviteURL: "https://some.domain.com", + SendSMS: &sendSMS, + }) + require.True(t, called) + require.NoError(t, err) + require.NotNil(t, res) + require.Len(t, res.CreatedUsers, 1) + require.Len(t, res.FailedUsers, 1) + assert.EqualValues(t, u1.Email, res.CreatedUsers[0].Email) + assert.EqualValues(t, u2.Email, res.FailedUsers[0].User.Email) + assert.EqualValues(t, "some failure", res.FailedUsers[0].Failure) +} + func TestUserCreateTestUserSuccess(t *testing.T) { response := map[string]any{ "user": map[string]any{ diff --git a/descope/sdk/mgmt.go b/descope/sdk/mgmt.go index b4cde5e8..5328487f 100644 --- a/descope/sdk/mgmt.go +++ b/descope/sdk/mgmt.go @@ -70,17 +70,28 @@ type User interface { // Those users are not counted as part of the monthly active users CreateTestUser(loginID string, user *descope.UserRequest) (*descope.UserResponse, error) - // Create a new user and invite them via an email message. + // Create a new user and invite via an email / text message. // // Functions exactly the same as the Create function with the additional invitation // behavior. See the documentation above for the general creation behavior. // - // IMPORTANT: Since the invitation is sent by email, make sure either - // the email is explicitly set, or the loginID itself is an email address. + // IMPORTANT: Since the invitation is sent by email / phone, make sure either + // the email / phone is explicitly set, or the loginID itself is an email address / phone number. // You must configure the invitation URL in the Descope console prior to // calling the method. Invite(loginID string, user *descope.UserRequest, options *descope.InviteOptions) (*descope.UserResponse, error) + // Create users in batch and invite them via an email / text message. + // + // Functions exactly the same as the Create function with the additional invitation + // behavior. See the documentation above for the general creation behavior. + // + // IMPORTANT: Since the invitation is sent by email / phone, make sure either + // the email / phone is explicitly set, or the loginID itself is an email address / phone number. + // You must configure the invitation URL in the Descope console prior to + // calling the method. + InviteBatch(users []*descope.BatchUser, options *descope.InviteOptions) (*descope.UsersBatchResponse, error) + // Update an existing user. // // The parameters follow the same convention as those for the Create function. diff --git a/descope/tests/mocks/mgmt/managementmock.go b/descope/tests/mocks/mgmt/managementmock.go index fd30f2b3..5d3114f5 100644 --- a/descope/tests/mocks/mgmt/managementmock.go +++ b/descope/tests/mocks/mgmt/managementmock.go @@ -148,6 +148,10 @@ type MockUser struct { InviteResponse *descope.UserResponse InviteError error + InviteBatchAssert func(users []*descope.BatchUser, options *descope.InviteOptions) + InviteBatchResponse *descope.UsersBatchResponse + InviteBatchError error + UpdateAssert func(loginID string, user *descope.UserRequest) UpdateResponse *descope.UserResponse UpdateError error @@ -274,6 +278,13 @@ func (m *MockUser) Invite(loginID string, user *descope.UserRequest, options *de return m.InviteResponse, m.InviteError } +func (m *MockUser) InviteBatch(users []*descope.BatchUser, options *descope.InviteOptions) (*descope.UsersBatchResponse, error) { + if m.InviteBatchAssert != nil { + m.InviteBatchAssert(users, options) + } + return m.InviteBatchResponse, m.InviteBatchError +} + func (m *MockUser) Update(loginID string, user *descope.UserRequest) (*descope.UserResponse, error) { if m.UpdateAssert != nil { m.UpdateAssert(loginID, user) diff --git a/descope/types.go b/descope/types.go index 40880f25..b43aee58 100644 --- a/descope/types.go +++ b/descope/types.go @@ -282,6 +282,11 @@ type UserRequest struct { VerifiedPhone *bool `json:"verifiedPhone,omitempty"` } +type BatchUser struct { + LoginID string `json:"loginId,omitempty"` + UserRequest `json:",inline"` +} + type UserResponse struct { User `json:",inline"` UserID string `json:"userId,omitempty"` @@ -302,6 +307,16 @@ type UserResponse struct { OAuth map[string]bool `json:"oauth,omitempty"` } +type UsersFailedResponse struct { + Failure string `json:"failure,omitempty"` + User *UserResponse `json:"user,omitempty"` +} + +type UsersBatchResponse struct { + CreatedUsers []*UserResponse `json:"createdUsers,omitempty"` + FailedUsers []*UsersFailedResponse `json:"failedUsers,omitempty"` +} + func (ur *UserResponse) GetCreatedTime() time.Time { return time.Unix(int64(ur.CreatedTime), 0) }