Skip to content

Commit

Permalink
Add support for batch user creation with passwords (#332)
Browse files Browse the repository at this point in the history
  • Loading branch information
shilgapira authored Nov 22, 2023
1 parent 84a80ac commit 3c75343
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 6 deletions.
30 changes: 29 additions & 1 deletion descope/internal/mgmt/user.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -24,6 +26,13 @@ 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) {
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) {
if user == nil {
user = &descope.UserRequest{}
Expand Down Expand Up @@ -469,7 +478,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,
Expand Down
38 changes: 34 additions & 4 deletions descope/internal/mgmt/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,39 +115,58 @@ func TestUsersInviteBatchSuccess(t *testing.T) {
u1.Email = "[email protected]"
u1.Roles = []string{"one"}
u1.CustomAttributes = ca
u1.Password = &descope.BatchUserPassword{Cleartext: "foo"}

u2 := &descope.BatchUser{}
u2.LoginID = "two"
u2.Email = "[email protected]"
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])

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])
Expand All @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions descope/sdk/mgmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions descope/tests/mocks/mgmt/managementmock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 23 additions & 1 deletion descope/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down

0 comments on commit 3c75343

Please sign in to comment.