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

Add support for batch user creation with passwords #332

Merged
merged 2 commits into from
Nov 22, 2023
Merged
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
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