Skip to content

Commit

Permalink
Merge branch 'main' into feat/authz
Browse files Browse the repository at this point in the history
  • Loading branch information
slavikm authored Nov 2, 2023
2 parents 1d0d9ad + 6f04bc7 commit 562cc0e
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 5 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,22 @@ userReqInvite.Tenants = []*descope.AssociatedTenant{
options := &descope.InviteOptions{InviteURL: "https://sub.domain.com"}
err := descopeClient.Management.User().Invite("[email protected]", userReqInvite, options)

// batch invite
options := &descope.InviteOptions{InviteURL: "https://sub.domain.com"}
batchUsers := []*descope.BatchUser{}
u1 := &descope.BatchUser{}
u1.LoginID = "one"
u1.Email = "[email protected]"
u1.Roles = []string{"one"}

u2 := &descope.BatchUser{}
u2.LoginID = "two"
u2.Email = "[email protected]"
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 = "[email protected]"
Expand Down
6 changes: 6 additions & 0 deletions descope/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -220,6 +221,7 @@ type mgmtEndpoints struct {
tenantSearchAll string

userCreate string
userCreateBatch string
userUpdate string
userDelete string
userDeleteAllTestUsers string
Expand Down Expand Up @@ -468,6 +470,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)
}
Expand Down
59 changes: 58 additions & 1 deletion descope/internal/mgmt/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -416,11 +432,43 @@ 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
}

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,
Expand Down Expand Up @@ -495,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
Expand Down
91 changes: 90 additions & 1 deletion descope/internal/mgmt/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func TestUserCreateSuccess(t *testing.T) {
"email": "[email protected]",
}}
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{}
Expand All @@ -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 = "[email protected]"
Expand All @@ -42,7 +52,9 @@ func TestUserCreateSuccess(t *testing.T) {
require.NotNil(t, res)
require.Equal(t, "[email protected]", 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, "[email protected]", res.Email)
Expand All @@ -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 = "[email protected]"
Expand All @@ -78,6 +92,81 @@ func TestUserCreateSuccessWithOptions(t *testing.T) {
require.Equal(t, "[email protected]", res.Email)
}

func TestUsersInviteBatchSuccess(t *testing.T) {
response := map[string]any{
"createdUsers": []map[string]any{
{"email": "[email protected]"},
},
"failedUsers": []map[string]any{
{
"user": map[string]any{
"email": "[email protected]",
},
"failure": "some failure",
},
},
}
ca := map[string]any{"ak": "av"}

users := []*descope.BatchUser{}

u1 := &descope.BatchUser{}
u1.LoginID = "one"
u1.Email = "[email protected]"
u1.Roles = []string{"one"}
u1.CustomAttributes = ca

u2 := &descope.BatchUser{}
u2.LoginID = "two"
u2.Email = "[email protected]"
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{
Expand Down
17 changes: 14 additions & 3 deletions descope/sdk/mgmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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 @@ -153,6 +153,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
Expand Down Expand Up @@ -279,6 +283,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)
Expand Down
17 changes: 17 additions & 0 deletions descope/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -280,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"`
Expand All @@ -300,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)
}
Expand Down

0 comments on commit 562cc0e

Please sign in to comment.