From 7eebbc02005e7689d1f9f4991a858722ad722aa6 Mon Sep 17 00:00:00 2001 From: Gil Shapira Date: Wed, 22 Nov 2023 12:38:13 +0200 Subject: [PATCH 1/2] Add support for batch user creation with passwords --- descope/internal/mgmt/user.go | 32 +++++++++++++++--- descope/internal/mgmt/user_test.go | 38 +++++++++++++++++++--- descope/sdk/mgmt.go | 6 ++++ descope/tests/mocks/mgmt/managementmock.go | 11 +++++++ descope/types.go | 24 +++++++++++++- 5 files changed, 101 insertions(+), 10 deletions(-) diff --git a/descope/internal/mgmt/user.go b/descope/internal/mgmt/user.go index 8faae35d..516ad7b9 100644 --- a/descope/internal/mgmt/user.go +++ b/descope/internal/mgmt/user.go @@ -1,6 +1,8 @@ package mgmt import ( + "encoding/base64" + "github.com/descope/go-sdk/descope" "github.com/descope/go-sdk/descope/api" "github.com/descope/go-sdk/descope/internal/utils" @@ -11,10 +13,7 @@ type user struct { } func (u *user) Create(loginID string, user *descope.UserRequest) (*descope.UserResponse, error) { - if user == nil { - user = &descope.UserRequest{} - } - return u.create(loginID, user.Email, user.Phone, user.Name, user.Picture, user.Roles, user.Tenants, false, false, user.CustomAttributes, user.VerifiedEmail, user.VerifiedPhone, nil) + return u.Invite(loginID, user, nil) } func (u *user) CreateTestUser(loginID string, user *descope.UserRequest) (*descope.UserResponse, error) { @@ -24,6 +23,10 @@ func (u *user) CreateTestUser(loginID string, user *descope.UserRequest) (*desco return u.create(loginID, user.Email, user.Phone, user.Name, user.Picture, user.Roles, user.Tenants, false, true, user.CustomAttributes, user.VerifiedEmail, user.VerifiedPhone, nil) } +func (u *user) CreateBatch(users []*descope.BatchUser) (*descope.UsersBatchResponse, error) { + return u.InviteBatch(users, nil) +} + func (u *user) Invite(loginID string, user *descope.UserRequest, options *descope.InviteOptions) (*descope.UserResponse, error) { if user == nil { user = &descope.UserRequest{} @@ -469,7 +472,26 @@ func makeCreateUserRequest(loginID, email, phone, displayName, picture string, r 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)) + user := makeUpdateUserRequest(u.LoginID, u.Email, u.Phone, u.Name, u.Picture, u.Roles, u.Tenants, u.CustomAttributes, u.VerifiedEmail, u.VerifiedPhone) + if u.Password != nil { + if cleartext := u.Password.Cleartext; cleartext != "" { + user["password"] = u.Password.Cleartext + } + if hashed := u.Password.Hashed; hashed != nil { + m := map[string]any{ + "algorithm": hashed.Algorithm, + "hash": base64.RawStdEncoding.EncodeToString(hashed.Hash), + } + if len(hashed.Salt) > 0 { + m["salt"] = base64.RawStdEncoding.EncodeToString(hashed.Salt) + } + if hashed.Iterations != 0 { + m["iterations"] = hashed.Iterations + } + user["hashedPassword"] = m + } + } + usersReq = append(usersReq, user) } req := map[string]any{ "users": usersReq, diff --git a/descope/internal/mgmt/user_test.go b/descope/internal/mgmt/user_test.go index a598b866..310b3cb5 100644 --- a/descope/internal/mgmt/user_test.go +++ b/descope/internal/mgmt/user_test.go @@ -115,32 +115,45 @@ func TestUsersInviteBatchSuccess(t *testing.T) { u1.Email = "one@one.com" u1.Roles = []string{"one"} u1.CustomAttributes = ca + u1.Password = &descope.BatchUserPassword{Cleartext: "foo"} u2 := &descope.BatchUser{} u2.LoginID = "two" u2.Email = "two@two.com" u2.Roles = []string{"two"} + u2.Password = &descope.BatchUserPassword{Hashed: &descope.BatchUserPasswordHashed{ + Algorithm: descope.BatchUserPasswordAlgorithmPBKDF2SHA256, + Hash: []byte("1"), + Salt: []byte("2"), + Iterations: 100, + }} users = append(users, u1, u2) sendSMS := true called := false + invite := true 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"]) + if invite { + 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"]) + } else { + assert.Nil(t, req["invite"]) + } 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"]) + require.Equal(t, "foo", userRes1["password"]) roleNames := userRes1["roleNames"].([]any) require.Len(t, roleNames, 1) require.Equal(t, u1.Roles[0], roleNames[0]) @@ -148,6 +161,12 @@ func TestUsersInviteBatchSuccess(t *testing.T) { require.Equal(t, u2.LoginID, userRes2["loginId"]) require.Equal(t, u2.Email, userRes2["email"]) assert.Nil(t, userRes2["customAttributes"]) + pass2, _ := userRes2["hashedPassword"].(map[string]any) + require.NotNil(t, pass2) + require.Equal(t, "pbkdf2sha256", pass2["algorithm"]) + require.Equal(t, "MQ", pass2["hash"]) + require.Equal(t, "Mg", pass2["salt"]) + require.EqualValues(t, 100, pass2["iterations"]) roleNames = userRes2["roleNames"].([]any) require.Len(t, roleNames, 1) require.Equal(t, u2.Roles[0], roleNames[0]) @@ -165,6 +184,17 @@ func TestUsersInviteBatchSuccess(t *testing.T) { 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) + + invite = false + res, err = m.User().CreateBatch(users) + 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) { diff --git a/descope/sdk/mgmt.go b/descope/sdk/mgmt.go index b84b92dd..98fbdbaf 100644 --- a/descope/sdk/mgmt.go +++ b/descope/sdk/mgmt.go @@ -70,6 +70,12 @@ 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 users in batch. + // + // Functions exactly the same as the Create function with the additional behavior that + // users can be created with a cleartext or hashed password. + CreateBatch(users []*descope.BatchUser) (*descope.UsersBatchResponse, error) + // Create a new user and invite via an email / text message. // // Functions exactly the same as the Create function with the additional invitation diff --git a/descope/tests/mocks/mgmt/managementmock.go b/descope/tests/mocks/mgmt/managementmock.go index a4063bf4..8de8a6b7 100644 --- a/descope/tests/mocks/mgmt/managementmock.go +++ b/descope/tests/mocks/mgmt/managementmock.go @@ -149,6 +149,10 @@ type MockUser struct { CreateTestUserResponse *descope.UserResponse CreateTestUserError error + CreateBatchAssert func(users []*descope.BatchUser) + CreateBatchResponse *descope.UsersBatchResponse + CreateBatchError error + InviteAssert func(loginID string, user *descope.UserRequest, options *descope.InviteOptions) InviteResponse *descope.UserResponse InviteError error @@ -280,6 +284,13 @@ func (m *MockUser) CreateTestUser(loginID string, user *descope.UserRequest) (*d return m.CreateTestUserResponse, m.CreateTestUserError } +func (m *MockUser) CreateBatch(users []*descope.BatchUser) (*descope.UsersBatchResponse, error) { + if m.CreateBatchAssert != nil { + m.CreateBatchAssert(users) + } + return m.CreateBatchResponse, m.CreateBatchError +} + func (m *MockUser) Invite(loginID string, user *descope.UserRequest, options *descope.InviteOptions) (*descope.UserResponse, error) { if m.InviteAssert != nil { m.InviteAssert(loginID, user, options) diff --git a/descope/types.go b/descope/types.go index 9695c639..f36718a4 100644 --- a/descope/types.go +++ b/descope/types.go @@ -283,10 +283,32 @@ type UserRequest struct { } type BatchUser struct { - LoginID string `json:"loginId,omitempty"` + LoginID string `json:"loginId,omitempty"` + Password *BatchUserPassword `json:"password,omitempty"` UserRequest `json:",inline"` } +type BatchUserPassword struct { + Cleartext string + Hashed *BatchUserPasswordHashed +} + +type BatchUserPasswordHashed struct { + Algorithm BatchUserPasswordAlgorithm + Hash []byte + Salt []byte + Iterations int +} + +type BatchUserPasswordAlgorithm string + +const ( + BatchUserPasswordAlgorithmBcrypt BatchUserPasswordAlgorithm = "bcrypt" + BatchUserPasswordAlgorithmPBKDF2SHA1 BatchUserPasswordAlgorithm = "pbkdf2sha1" + BatchUserPasswordAlgorithmPBKDF2SHA256 BatchUserPasswordAlgorithm = "pbkdf2sha256" + BatchUserPasswordAlgorithmPBKDF2SHA512 BatchUserPasswordAlgorithm = "pbkdf2sha512" +) + type UserResponse struct { User `json:",inline"` UserID string `json:"userId,omitempty"` From 653306bf91fd075dcb55c7cb921ebbc69eead531 Mon Sep 17 00:00:00 2001 From: Gil Shapira Date: Wed, 22 Nov 2023 13:31:44 +0200 Subject: [PATCH 2/2] Fix incorrect overload --- descope/internal/mgmt/user.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/descope/internal/mgmt/user.go b/descope/internal/mgmt/user.go index 516ad7b9..954bfacb 100644 --- a/descope/internal/mgmt/user.go +++ b/descope/internal/mgmt/user.go @@ -13,7 +13,10 @@ type user struct { } func (u *user) Create(loginID string, user *descope.UserRequest) (*descope.UserResponse, error) { - return u.Invite(loginID, user, nil) + if user == nil { + user = &descope.UserRequest{} + } + return u.create(loginID, user.Email, user.Phone, user.Name, user.Picture, user.Roles, user.Tenants, false, false, user.CustomAttributes, user.VerifiedEmail, user.VerifiedPhone, nil) } func (u *user) CreateTestUser(loginID string, user *descope.UserRequest) (*descope.UserResponse, error) { @@ -24,7 +27,10 @@ func (u *user) CreateTestUser(loginID string, user *descope.UserRequest) (*desco } func (u *user) CreateBatch(users []*descope.BatchUser) (*descope.UsersBatchResponse, error) { - return u.InviteBatch(users, nil) + if users == nil { + users = []*descope.BatchUser{} + } + return u.createBatch(users, nil) } func (u *user) Invite(loginID string, user *descope.UserRequest, options *descope.InviteOptions) (*descope.UserResponse, error) {