feat: user v3 contact email and phone (#8644)

# Which Problems Are Solved

Endpoints to maintain email and phone contact on user v3 are not
implemented.

# How the Problems Are Solved

Add 3 endpoints with SetContactEmail, VerifyContactEmail and
ResendContactEmailCode.
Add 3 endpoints with SetContactPhone, VerifyContactPhone and
ResendContactPhoneCode.
Refactor the logic how contact is managed in the user creation and
update.

# Additional Changes

None

# Additional Context

- part of https://github.com/zitadel/zitadel/issues/6433

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz 2024-09-25 15:31:31 +02:00 committed by GitHub
parent 624fee97c0
commit 62cdec222e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 4924 additions and 432 deletions

View File

@ -454,7 +454,7 @@ func startAPIs(
if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, user_v3_alpha.CreateServer(commands, keys.User)); err != nil {
if err := apis.RegisterService(ctx, user_v3_alpha.CreateServer(commands)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, webkey.CreateServer(commands, queries)); err != nil {

View File

@ -0,0 +1,83 @@
package user
import (
"context"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
)
func (s *Server) SetContactEmail(ctx context.Context, req *user.SetContactEmailRequest) (_ *user.SetContactEmailResponse, err error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
schemauser := setContactEmailRequestToChangeSchemaUserEmail(req)
details, err := s.command.ChangeSchemaUserEmail(ctx, schemauser)
if err != nil {
return nil, err
}
return &user.SetContactEmailResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner),
VerificationCode: schemauser.ReturnCode,
}, nil
}
func setContactEmailRequestToChangeSchemaUserEmail(req *user.SetContactEmailRequest) *command.ChangeSchemaUserEmail {
return &command.ChangeSchemaUserEmail{
ResourceOwner: organizationToUpdateResourceOwner(req.Organization),
ID: req.GetId(),
Email: setEmailToEmail(req.Email),
}
}
func setEmailToEmail(setEmail *user.SetEmail) *command.Email {
if setEmail == nil {
return nil
}
return &command.Email{
Address: domain.EmailAddress(setEmail.Address),
ReturnCode: setEmail.GetReturnCode() != nil,
Verified: setEmail.GetIsVerified(),
URLTemplate: setEmail.GetSendCode().GetUrlTemplate(),
}
}
func (s *Server) VerifyContactEmail(ctx context.Context, req *user.VerifyContactEmailRequest) (_ *user.VerifyContactEmailResponse, err error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.VerifySchemaUserEmail(ctx, organizationToUpdateResourceOwner(req.Organization), req.GetId(), req.GetVerificationCode())
if err != nil {
return nil, err
}
return &user.VerifyContactEmailResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner),
}, nil
}
func (s *Server) ResendContactEmailCode(ctx context.Context, req *user.ResendContactEmailCodeRequest) (_ *user.ResendContactEmailCodeResponse, err error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
schemauser := resendContactEmailCodeRequestToResendSchemaUserEmailCode(req)
details, err := s.command.ResendSchemaUserEmailCode(ctx, schemauser)
if err != nil {
return nil, err
}
return &user.ResendContactEmailCodeResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner),
VerificationCode: schemauser.PlainCode,
}, nil
}
func resendContactEmailCodeRequestToResendSchemaUserEmailCode(req *user.ResendContactEmailCodeRequest) *command.ResendSchemaUserEmailCode {
return &command.ResendSchemaUserEmailCode{
ResourceOwner: organizationToUpdateResourceOwner(req.Organization),
ID: req.GetId(),
URLTemplate: req.GetSendCode().GetUrlTemplate(),
ReturnCode: req.GetReturnCode() != nil,
}
}

View File

@ -0,0 +1,772 @@
//go:build integration
package user_test
import (
"context"
"testing"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
)
func TestServer_SetContactEmail(t *testing.T) {
t.Parallel()
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
schema := []byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`)
schemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema)
orgResp := instance.CreateOrganization(isolatedIAMOwnerCTX, gofakeit.Name(), gofakeit.Email())
type res struct {
want *resource_object.Details
returnCode bool
}
tests := []struct {
name string
ctx context.Context
dep func(req *user.SetContactEmailRequest) error
req *user.SetContactEmailRequest
res res
wantErr bool
}{
{
name: "email patch, no context",
ctx: context.Background(),
dep: func(req *user.SetContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Email: &user.SetEmail{
Address: gofakeit.Email(),
},
},
wantErr: true,
},
{
name: "email patch, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
dep: func(req *user.SetContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Email: &user.SetEmail{
Address: gofakeit.Email(),
},
},
wantErr: true,
},
{
name: "email patch, not found",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactEmailRequest) error {
return nil
},
req: &user.SetContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Id: "notexisting",
Email: &user.SetEmail{
Address: gofakeit.Email(),
},
},
wantErr: true,
},
{
name: "email patch, not found, org",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: "not existing",
},
},
Email: &user.SetEmail{
Address: gofakeit.Email(),
},
},
wantErr: true,
},
{
name: "email patch, empty",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactEmailRequest) error {
data := "{\"name\": \"user\"}"
schemaID := schemaResp.GetDetails().GetId()
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaID, []byte(data))
req.Id = userResp.GetDetails().GetId()
email := gofakeit.Email()
instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, email)
return nil
},
req: &user.SetContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Email: &user.SetEmail{},
},
wantErr: true,
},
{
name: "email patch, no change",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactEmailRequest) error {
data := "{\"name\": \"user\"}"
schemaID := schemaResp.GetDetails().GetId()
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaID, []byte(data))
req.Id = userResp.GetDetails().GetId()
email := gofakeit.Email()
instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, email)
req.Email.Address = email
return nil
},
req: &user.SetContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Email: &user.SetEmail{},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "email patch, no org, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactEmailRequest{
Email: &user.SetEmail{
Address: gofakeit.Email(),
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "email patch, return, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Email: &user.SetEmail{
Address: gofakeit.Email(),
Verification: &user.SetEmail_ReturnCode{},
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
returnCode: true,
},
},
{
name: "email patch, return, invalid template",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Email: &user.SetEmail{
Address: gofakeit.Email(),
Verification: &user.SetEmail_SendCode{SendCode: &user.SendEmailVerificationCode{UrlTemplate: gu.Ptr("{{")}},
},
},
wantErr: true,
},
{
name: "email patch, verified, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Email: &user.SetEmail{
Address: gofakeit.Email(),
Verification: &user.SetEmail_IsVerified{IsVerified: true},
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "email patch, template, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Email: &user.SetEmail{
Address: gofakeit.Email(),
Verification: &user.SetEmail_SendCode{SendCode: &user.SendEmailVerificationCode{UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}")}},
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "email patch, sent, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Email: &user.SetEmail{
Address: gofakeit.Email(),
Verification: &user.SetEmail_SendCode{},
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.dep != nil {
err := tt.dep(tt.req)
require.NoError(t, err)
}
got, err := instance.Client.UserV3Alpha.SetContactEmail(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.res.want, got.Details)
if tt.res.returnCode {
assert.NotNil(t, got.VerificationCode)
} else {
assert.Nil(t, got.VerificationCode)
}
})
}
}
func TestServer_VerifyContactEmail(t *testing.T) {
t.Parallel()
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
schema := []byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`)
schemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema)
orgResp := instance.CreateOrganization(isolatedIAMOwnerCTX, gofakeit.Name(), gofakeit.Email())
type res struct {
want *resource_object.Details
}
tests := []struct {
name string
ctx context.Context
dep func(req *user.VerifyContactEmailRequest) error
req *user.VerifyContactEmailRequest
res res
wantErr bool
}{
{
name: "email verify, no context",
ctx: context.Background(),
dep: func(req *user.VerifyContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
verifyResp := instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
req.VerificationCode = verifyResp.GetVerificationCode()
return nil
},
req: &user.VerifyContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "email verify, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
dep: func(req *user.VerifyContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
verifyResp := instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
req.VerificationCode = verifyResp.GetVerificationCode()
return nil
},
req: &user.VerifyContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "email verify, not found",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.VerifyContactEmailRequest) error {
return nil
},
req: &user.VerifyContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Id: "notexisting",
VerificationCode: "unimportant",
},
wantErr: true,
},
{
name: "email verify, not found, org",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.VerifyContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
verifyResp := instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
req.VerificationCode = verifyResp.GetVerificationCode()
return nil
},
req: &user.VerifyContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: "not existing",
},
},
},
wantErr: true,
},
{
name: "email verify, wrong code",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.VerifyContactEmailRequest) error {
data := "{\"name\": \"user\"}"
schemaID := schemaResp.GetDetails().GetId()
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaID, []byte(data))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
return nil
},
req: &user.VerifyContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
VerificationCode: "wrong",
},
wantErr: true,
},
{
name: "email verify, no org, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.VerifyContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
verifyResp := instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
req.VerificationCode = verifyResp.GetVerificationCode()
return nil
},
req: &user.VerifyContactEmailRequest{},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "email verify, return, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.VerifyContactEmailRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
verifyResp := instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
req.VerificationCode = verifyResp.GetVerificationCode()
return nil
},
req: &user.VerifyContactEmailRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.dep != nil {
err := tt.dep(tt.req)
require.NoError(t, err)
}
got, err := instance.Client.UserV3Alpha.VerifyContactEmail(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.res.want, got.Details)
})
}
}
func TestServer_ResendContactEmailCode(t *testing.T) {
t.Parallel()
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
schema := []byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`)
schemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema)
orgResp := instance.CreateOrganization(isolatedIAMOwnerCTX, gofakeit.Name(), gofakeit.Email())
type res struct {
want *resource_object.Details
returnCode bool
}
tests := []struct {
name string
ctx context.Context
dep func(req *user.ResendContactEmailCodeRequest) error
req *user.ResendContactEmailCodeRequest
res res
wantErr bool
}{
{
name: "email resend, no context",
ctx: context.Background(),
dep: func(req *user.ResendContactEmailCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
return nil
},
req: &user.ResendContactEmailCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "email resend, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
dep: func(req *user.ResendContactEmailCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
return nil
},
req: &user.ResendContactEmailCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "email resend, not found",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactEmailCodeRequest) error {
return nil
},
req: &user.ResendContactEmailCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Id: "notexisting",
},
wantErr: true,
},
{
name: "email resend, not found, org",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactEmailCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
return nil
},
req: &user.ResendContactEmailCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: "not existing",
},
},
},
wantErr: true,
},
{
name: "email resend, no code",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactEmailCodeRequest) error {
data := "{\"name\": \"user\"}"
schemaID := schemaResp.GetDetails().GetId()
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaID, []byte(data))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.ResendContactEmailCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "email resend, no org, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactEmailCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
return nil
},
req: &user.ResendContactEmailCodeRequest{},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "email resend, return, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactEmailCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
return nil
},
req: &user.ResendContactEmailCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Verification: &user.ResendContactEmailCodeRequest_ReturnCode{},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
returnCode: true,
},
},
{
name: "email resend, sent, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactEmailCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserEmail(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Email())
return nil
},
req: &user.ResendContactEmailCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Verification: &user.ResendContactEmailCodeRequest_SendCode{},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.dep != nil {
err := tt.dep(tt.req)
require.NoError(t, err)
}
got, err := instance.Client.UserV3Alpha.ResendContactEmailCode(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.res.want, got.Details)
if tt.res.returnCode {
assert.NotNil(t, got.VerificationCode)
} else {
assert.Nil(t, got.VerificationCode)
}
})
}
}

View File

@ -0,0 +1,701 @@
//go:build integration
package user_test
import (
"context"
"testing"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
)
func TestServer_SetContactPhone(t *testing.T) {
t.Parallel()
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
schema := []byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`)
schemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema)
orgResp := instance.CreateOrganization(isolatedIAMOwnerCTX, gofakeit.Name(), gofakeit.Email())
type res struct {
want *resource_object.Details
returnCode bool
}
tests := []struct {
name string
ctx context.Context
dep func(req *user.SetContactPhoneRequest) error
req *user.SetContactPhoneRequest
res res
wantErr bool
}{
{
name: "phone patch, no context",
ctx: context.Background(),
dep: func(req *user.SetContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Phone: &user.SetPhone{
Number: gofakeit.Phone(),
},
},
wantErr: true,
},
{
name: "phone patch, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
dep: func(req *user.SetContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Phone: &user.SetPhone{
Number: gofakeit.Phone(),
},
},
wantErr: true,
},
{
name: "phone patch, not found",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactPhoneRequest) error {
return nil
},
req: &user.SetContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Id: "notexisting",
Phone: &user.SetPhone{
Number: gofakeit.Phone(),
},
},
wantErr: true,
},
{
name: "phone patch, not found, org",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: "not existing",
},
},
Phone: &user.SetPhone{
Number: gofakeit.Phone(),
},
},
wantErr: true,
},
{
name: "phone patch, no change",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactPhoneRequest) error {
data := "{\"name\": \"user\"}"
schemaID := schemaResp.GetDetails().GetId()
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaID, []byte(data))
req.Id = userResp.GetDetails().GetId()
number := gofakeit.Phone()
instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, number)
req.Phone.Number = number
return nil
},
req: &user.SetContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Phone: &user.SetPhone{},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "phone patch, no org, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactPhoneRequest{
Phone: &user.SetPhone{
Number: gofakeit.Phone(),
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "phone patch, return, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Phone: &user.SetPhone{
Number: gofakeit.Phone(),
Verification: &user.SetPhone_ReturnCode{},
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
returnCode: true,
},
},
{
name: "phone patch, verified, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Phone: &user.SetPhone{
Number: gofakeit.Phone(),
Verification: &user.SetPhone_IsVerified{IsVerified: true},
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "phone patch, sent, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.SetContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.SetContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Phone: &user.SetPhone{
Number: gofakeit.Phone(),
Verification: &user.SetPhone_SendCode{},
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.dep != nil {
err := tt.dep(tt.req)
require.NoError(t, err)
}
got, err := instance.Client.UserV3Alpha.SetContactPhone(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.res.want, got.Details)
if tt.res.returnCode {
assert.NotNil(t, got.VerificationCode)
} else {
assert.Nil(t, got.VerificationCode)
}
})
}
}
func TestServer_VerifyContactPhone(t *testing.T) {
t.Parallel()
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
schema := []byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`)
schemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema)
orgResp := instance.CreateOrganization(isolatedIAMOwnerCTX, gofakeit.Name(), gofakeit.Email())
type res struct {
want *resource_object.Details
returnCodePhone bool
}
tests := []struct {
name string
ctx context.Context
dep func(req *user.VerifyContactPhoneRequest) error
req *user.VerifyContactPhoneRequest
res res
wantErr bool
}{
{
name: "phone verify, no context",
ctx: context.Background(),
dep: func(req *user.VerifyContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
verifyResp := instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
req.VerificationCode = verifyResp.GetVerificationCode()
return nil
},
req: &user.VerifyContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "phone verify, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
dep: func(req *user.VerifyContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
verifyResp := instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
req.VerificationCode = verifyResp.GetVerificationCode()
return nil
},
req: &user.VerifyContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "phone verify, not found",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.VerifyContactPhoneRequest) error {
return nil
},
req: &user.VerifyContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Id: "notexisting",
VerificationCode: "unimportant",
},
wantErr: true,
},
{
name: "phone verify, not found, org",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.VerifyContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
verifyResp := instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
req.VerificationCode = verifyResp.GetVerificationCode()
return nil
},
req: &user.VerifyContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: "not existing",
},
},
},
wantErr: true,
},
{
name: "phone verify, wrong code",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.VerifyContactPhoneRequest) error {
data := "{\"name\": \"user\"}"
schemaID := schemaResp.GetDetails().GetId()
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaID, []byte(data))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
return nil
},
req: &user.VerifyContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
VerificationCode: "wrong",
},
wantErr: true,
},
{
name: "phone verify, no org, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.VerifyContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
verifyResp := instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
req.VerificationCode = verifyResp.GetVerificationCode()
return nil
},
req: &user.VerifyContactPhoneRequest{},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "phone verify, return, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.VerifyContactPhoneRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
verifyResp := instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
req.VerificationCode = verifyResp.GetVerificationCode()
return nil
},
req: &user.VerifyContactPhoneRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
returnCodePhone: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.dep != nil {
err := tt.dep(tt.req)
require.NoError(t, err)
}
got, err := instance.Client.UserV3Alpha.VerifyContactPhone(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.res.want, got.Details)
})
}
}
func TestServer_ResendContactPhoneCode(t *testing.T) {
t.Parallel()
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
schema := []byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`)
schemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema)
orgResp := instance.CreateOrganization(isolatedIAMOwnerCTX, gofakeit.Name(), gofakeit.Email())
type res struct {
want *resource_object.Details
returnCode bool
}
tests := []struct {
name string
ctx context.Context
dep func(req *user.ResendContactPhoneCodeRequest) error
req *user.ResendContactPhoneCodeRequest
res res
wantErr bool
}{
{
name: "phone resend, no context",
ctx: context.Background(),
dep: func(req *user.ResendContactPhoneCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
return nil
},
req: &user.ResendContactPhoneCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "phone resend, no permission",
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
dep: func(req *user.ResendContactPhoneCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
return nil
},
req: &user.ResendContactPhoneCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "phone resend, not found",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactPhoneCodeRequest) error {
return nil
},
req: &user.ResendContactPhoneCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Id: "notexisting",
},
wantErr: true,
},
{
name: "phone resend, not found, org",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactPhoneCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
return nil
},
req: &user.ResendContactPhoneCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: "not existing",
},
},
},
wantErr: true,
},
{
name: "phone resend, no code",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactPhoneCodeRequest) error {
data := "{\"name\": \"user\"}"
schemaID := schemaResp.GetDetails().GetId()
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaID, []byte(data))
req.Id = userResp.GetDetails().GetId()
return nil
},
req: &user.ResendContactPhoneCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
},
wantErr: true,
},
{
name: "phone resend, no org, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactPhoneCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
return nil
},
req: &user.ResendContactPhoneCodeRequest{},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
{
name: "phone resend, return, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactPhoneCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
return nil
},
req: &user.ResendContactPhoneCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Verification: &user.ResendContactPhoneCodeRequest_ReturnCode{},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
returnCode: true,
},
},
{
name: "phone resend, sent, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(req *user.ResendContactPhoneCodeRequest) error {
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
req.Id = userResp.GetDetails().GetId()
instance.UpdateSchemaUserPhone(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Phone())
return nil
},
req: &user.ResendContactPhoneCodeRequest{
Organization: &object.Organization{
Property: &object.Organization_OrgId{
OrgId: orgResp.GetOrganizationId(),
},
},
Verification: &user.ResendContactPhoneCodeRequest_SendCode{},
},
res: res{
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_ORG,
Id: orgResp.GetOrganizationId(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.dep != nil {
err := tt.dep(tt.req)
require.NoError(t, err)
}
got, err := instance.Client.UserV3Alpha.ResendContactPhoneCode(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.res.want, got.Details)
if tt.res.returnCode {
assert.NotNil(t, got.VerificationCode)
} else {
assert.Nil(t, got.VerificationCode)
}
})
}
}

View File

@ -14,6 +14,7 @@ import (
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
)
var (
@ -51,7 +52,7 @@ func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) {
f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(ttt, err)
assert.NoError(ttt, err)
if f.UserSchema.GetEnabled() {
return
}
@ -59,4 +60,13 @@ func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) {
retryDuration,
time.Second,
"timed out waiting for ensuring instance feature")
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
_, err := instance.Client.UserV3Alpha.SearchUsers(ctx, &user.SearchUsersRequest{})
assert.NoError(ttt, err)
},
retryDuration,
time.Second,
"timed out waiting for ensuring instance feature call")
}

View File

@ -8,6 +8,7 @@ import (
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/logging"
"google.golang.org/protobuf/types/known/structpb"
@ -628,16 +629,20 @@ func TestServer_PatchUser(t *testing.T) {
}
got, err := instance.Client.UserV3Alpha.PatchUser(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.res.want, got.Details)
if tt.res.returnCodeEmail {
require.NotNil(t, got.EmailCode)
assert.NotNil(t, got.EmailCode)
} else {
assert.Nil(t, got.EmailCode)
}
if tt.res.returnCodePhone {
require.NotNil(t, got.PhoneCode)
assert.NotNil(t, got.PhoneCode)
} else {
assert.Nil(t, got.PhoneCode)
}
})
}
@ -843,10 +848,10 @@ func TestServer_DeleteUser(t *testing.T) {
}
got, err := instance.Client.UserV3Alpha.DeleteUser(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
@ -1054,10 +1059,10 @@ func TestServer_LockUser(t *testing.T) {
}
got, err := instance.Client.UserV3Alpha.LockUser(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
@ -1237,10 +1242,10 @@ func TestServer_UnlockUser(t *testing.T) {
}
got, err := instance.Client.UserV3Alpha.UnlockUser(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
@ -1439,10 +1444,10 @@ func TestServer_DeactivateUser(t *testing.T) {
}
got, err := instance.Client.UserV3Alpha.DeactivateUser(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
@ -1622,10 +1627,10 @@ func TestServer_ActivateUser(t *testing.T) {
}
got, err := instance.Client.UserV3Alpha.ActivateUser(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}

View File

@ -0,0 +1,81 @@
package user
import (
"context"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
)
func (s *Server) SetContactPhone(ctx context.Context, req *user.SetContactPhoneRequest) (_ *user.SetContactPhoneResponse, err error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
schemauser := setContactPhoneRequestToChangeSchemaUserPhone(req)
details, err := s.command.ChangeSchemaUserPhone(ctx, schemauser)
if err != nil {
return nil, err
}
return &user.SetContactPhoneResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner),
VerificationCode: schemauser.ReturnCode,
}, nil
}
func setContactPhoneRequestToChangeSchemaUserPhone(req *user.SetContactPhoneRequest) *command.ChangeSchemaUserPhone {
return &command.ChangeSchemaUserPhone{
ResourceOwner: organizationToUpdateResourceOwner(req.Organization),
ID: req.GetId(),
Phone: setPhoneToPhone(req.Phone),
}
}
func setPhoneToPhone(setPhone *user.SetPhone) *command.Phone {
if setPhone == nil {
return nil
}
return &command.Phone{
Number: domain.PhoneNumber(setPhone.Number),
ReturnCode: setPhone.GetReturnCode() != nil,
Verified: setPhone.GetIsVerified(),
}
}
func (s *Server) VerifyContactPhone(ctx context.Context, req *user.VerifyContactPhoneRequest) (_ *user.VerifyContactPhoneResponse, err error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.VerifySchemaUserPhone(ctx, organizationToUpdateResourceOwner(req.Organization), req.GetId(), req.GetVerificationCode())
if err != nil {
return nil, err
}
return &user.VerifyContactPhoneResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner),
}, nil
}
func (s *Server) ResendContactPhoneCode(ctx context.Context, req *user.ResendContactPhoneCodeRequest) (_ *user.ResendContactPhoneCodeResponse, err error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
schemauser := resendContactPhoneCodeRequestToResendSchemaUserPhoneCode(req)
details, err := s.command.ResendSchemaUserPhoneCode(ctx, schemauser)
if err != nil {
return nil, err
}
return &user.ResendContactPhoneCodeResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner),
VerificationCode: schemauser.PlainCode,
}, nil
}
func resendContactPhoneCodeRequestToResendSchemaUserPhoneCode(req *user.ResendContactPhoneCodeRequest) *command.ResendSchemaUserPhoneCode {
return &command.ResendSchemaUserPhoneCode{
ResourceOwner: organizationToUpdateResourceOwner(req.Organization),
ID: req.GetId(),
ReturnCode: req.GetReturnCode() != nil,
}
}

View File

@ -0,0 +1,14 @@
package user
import (
"context"
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
)
func (s *Server) SearchUsers(ctx context.Context, _ *user.SearchUsersRequest) (_ *user.SearchUsersResponse, err error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
return &user.SearchUsersResponse{}, nil
}

View File

@ -6,7 +6,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
)
@ -14,19 +13,16 @@ var _ user.ZITADELUsersServer = (*Server)(nil)
type Server struct {
user.UnimplementedZITADELUsersServer
command *command.Commands
userCodeAlg crypto.EncryptionAlgorithm
command *command.Commands
}
type Config struct{}
func CreateServer(
command *command.Commands,
userCodeAlg crypto.EncryptionAlgorithm,
) *Server {
return &Server{
command: command,
userCodeAlg: userCodeAlg,
command: command,
}
}

View File

@ -3,12 +3,9 @@ package user
import (
"context"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
"github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
@ -22,14 +19,14 @@ func (s *Server) CreateUser(ctx context.Context, req *user.CreateUserRequest) (_
if err != nil {
return nil, err
}
if err := s.command.CreateSchemaUser(ctx, schemauser, s.userCodeAlg); err != nil {
details, err := s.command.CreateSchemaUser(ctx, schemauser)
if err != nil {
return nil, err
}
return &user.CreateUserResponse{
Details: resource_object.DomainToDetailsPb(schemauser.Details, object.OwnerType_OWNER_TYPE_ORG, schemauser.Details.ResourceOwner),
EmailCode: gu.Ptr(schemauser.ReturnCodeEmail),
PhoneCode: gu.Ptr(schemauser.ReturnCodePhone),
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner),
EmailCode: schemauser.ReturnCodeEmail,
PhoneCode: schemauser.ReturnCodePhone,
}, nil
}
@ -44,6 +41,8 @@ func createUserRequestToCreateSchemaUser(ctx context.Context, req *user.CreateUs
SchemaID: req.GetUser().GetSchemaId(),
ID: req.GetUser().GetUserId(),
Data: data,
Email: setEmailToEmail(req.GetUser().GetContact().GetEmail()),
Phone: setPhoneToPhone(req.GetUser().GetContact().GetPhone()),
}, nil
}
@ -91,17 +90,36 @@ func (s *Server) PatchUser(ctx context.Context, req *user.PatchUserRequest) (_ *
return nil, err
}
if err := s.command.ChangeSchemaUser(ctx, schemauser, s.userCodeAlg); err != nil {
details, err := s.command.ChangeSchemaUser(ctx, schemauser)
if err != nil {
return nil, err
}
return &user.PatchUserResponse{
Details: resource_object.DomainToDetailsPb(schemauser.Details, object.OwnerType_OWNER_TYPE_ORG, schemauser.Details.ResourceOwner),
EmailCode: gu.Ptr(schemauser.ReturnCodeEmail),
PhoneCode: gu.Ptr(schemauser.ReturnCodePhone),
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner),
EmailCode: schemauser.ReturnCodeEmail,
PhoneCode: schemauser.ReturnCodePhone,
}, nil
}
func patchUserRequestToChangeSchemaUser(req *user.PatchUserRequest) (_ *command.ChangeSchemaUser, err error) {
schemaUser, err := setSchemaUserToSchemaUser(req)
if err != nil {
return nil, err
}
email, phone := setContactToContact(req.GetUser().GetContact())
return &command.ChangeSchemaUser{
ResourceOwner: organizationToUpdateResourceOwner(req.Organization),
ID: req.GetId(),
SchemaUser: schemaUser,
Email: email,
Phone: phone,
}, nil
}
func setSchemaUserToSchemaUser(req *user.PatchUserRequest) (_ *command.SchemaUser, err error) {
if req.GetUser() == nil {
return nil, nil
}
var data []byte
if req.GetUser().Data != nil {
data, err = req.GetUser().GetData().MarshalJSON()
@ -110,45 +128,19 @@ func patchUserRequestToChangeSchemaUser(req *user.PatchUserRequest) (_ *command.
}
}
var email *command.Email
var phone *command.Phone
if req.GetUser().GetContact() != nil {
if req.GetUser().GetContact().GetEmail() != nil {
email = &command.Email{
Address: domain.EmailAddress(req.GetUser().GetContact().Email.Address),
}
if req.GetUser().GetContact().Email.GetIsVerified() {
email.Verified = true
}
if req.GetUser().GetContact().Email.GetReturnCode() != nil {
email.ReturnCode = true
}
if req.GetUser().GetContact().Email.GetSendCode() != nil {
email.URLTemplate = req.GetUser().GetContact().Email.GetSendCode().GetUrlTemplate()
}
}
if req.GetUser().GetContact().Phone != nil {
phone = &command.Phone{
Number: domain.PhoneNumber(req.GetUser().GetContact().Phone.Number),
}
if req.GetUser().GetContact().Phone.GetIsVerified() {
phone.Verified = true
}
if req.GetUser().GetContact().Phone.GetReturnCode() != nil {
phone.ReturnCode = true
}
}
}
return &command.ChangeSchemaUser{
ResourceOwner: organizationToUpdateResourceOwner(req.Organization),
ID: req.GetId(),
SchemaID: req.GetUser().SchemaId,
Data: data,
Email: email,
Phone: phone,
return &command.SchemaUser{
SchemaID: req.GetUser().GetSchemaId(),
Data: data,
}, nil
}
func setContactToContact(contact *user.SetContact) (*command.Email, *command.Phone) {
if contact == nil {
return nil, nil
}
return setEmailToEmail(contact.GetEmail()), setPhoneToPhone(contact.GetPhone())
}
func (s *Server) DeactivateUser(ctx context.Context, req *user.DeactivateUserRequest) (_ *user.DeactivateUserResponse, err error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err

View File

@ -14,6 +14,7 @@ import (
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha"
)
var (
@ -51,7 +52,7 @@ func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) {
f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(ttt, err)
assert.NoError(ttt, err)
if f.UserSchema.GetEnabled() {
return
}
@ -59,4 +60,13 @@ func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) {
retryDuration,
time.Second,
"timed out waiting for ensuring instance feature")
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
_, err := instance.Client.UserSchemaV3.SearchUserSchemas(ctx, &schema.SearchUserSchemasRequest{})
assert.NoError(ttt, err)
},
retryDuration,
time.Second,
"timed out waiting for ensuring instance feature call")
}

View File

@ -189,6 +189,9 @@ type AppendReducer interface {
}
func (c *Commands) pushAppendAndReduce(ctx context.Context, object AppendReducer, cmds ...eventstore.Command) error {
if len(cmds) == 0 {
return nil
}
events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return err
@ -196,6 +199,20 @@ func (c *Commands) pushAppendAndReduce(ctx context.Context, object AppendReducer
return AppendAndReduce(object, events...)
}
type AppendReducerDetails interface {
AppendEvents(...eventstore.Event)
// TODO: Why is it allowed to return an error here?
Reduce() error
GetWriteModel() *eventstore.WriteModel
}
func (c *Commands) pushAppendAndReduceDetails(ctx context.Context, object AppendReducerDetails, cmds ...eventstore.Command) (*domain.ObjectDetails, error) {
if err := c.pushAppendAndReduce(ctx, object, cmds...); err != nil {
return nil, err
}
return writeModelToObjectDetails(object.GetWriteModel()), nil
}
func AppendAndReduce(object AppendReducer, events ...eventstore.Event) error {
object.AppendEvents(events...)
return object.Reduce()

View File

@ -6,17 +6,13 @@ import (
"encoding/json"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
domain_schema "github.com/zitadel/zitadel/internal/domain/schema"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user/schemauser"
"github.com/zitadel/zitadel/internal/zerrors"
)
type CreateSchemaUser struct {
Details *domain.ObjectDetails
SchemaID string
schemaRevision uint64
@ -25,9 +21,9 @@ type CreateSchemaUser struct {
Data json.RawMessage
Email *Email
ReturnCodeEmail string
ReturnCodeEmail *string
Phone *Phone
ReturnCodePhone string
ReturnCodePhone *string
}
func (s *CreateSchemaUser) Valid(ctx context.Context, c *Commands) (err error) {
@ -99,44 +95,36 @@ func (c *Commands) getSchemaRoleForWrite(ctx context.Context, resourceOwner, use
return domain_schema.RoleOwner, nil
}
func (c *Commands) CreateSchemaUser(ctx context.Context, user *CreateSchemaUser, alg crypto.EncryptionAlgorithm) (err error) {
func (c *Commands) CreateSchemaUser(ctx context.Context, user *CreateSchemaUser) (*domain.ObjectDetails, error) {
if err := user.Valid(ctx, c); err != nil {
return err
return nil, err
}
writeModel, err := c.getSchemaUserExists(ctx, user.ResourceOwner, user.ID)
if err != nil {
return err
}
if writeModel.Exists() {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.AlreadyExists")
return nil, err
}
userAgg := UserV3AggregateFromWriteModel(&writeModel.WriteModel)
events := []eventstore.Command{
schemauser.NewCreatedEvent(ctx,
userAgg,
user.SchemaID, user.schemaRevision, user.Data,
),
events, codeEmail, codePhone, err := writeModel.NewCreated(ctx,
user.SchemaID,
user.schemaRevision,
user.Data,
user.Email,
user.Phone,
func(ctx context.Context) (*EncryptedCode, error) {
return c.newEmailCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck
},
)
if err != nil {
return nil, err
}
if user.Email != nil {
events, user.ReturnCodeEmail, err = c.updateSchemaUserEmail(ctx, writeModel, events, userAgg, user.Email, alg)
if err != nil {
return err
}
if codeEmail != "" {
user.ReturnCodeEmail = &codeEmail
}
if user.Phone != nil {
events, user.ReturnCodePhone, err = c.updateSchemaUserPhone(ctx, writeModel, events, userAgg, user.Phone, alg)
if err != nil {
return err
}
if codePhone != "" {
user.ReturnCodePhone = &codePhone
}
if err := c.pushAppendAndReduce(ctx, writeModel, events...); err != nil {
return err
}
user.Details = writeModelToObjectDetails(&writeModel.WriteModel)
return nil
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}
func (c *Commands) DeleteSchemaUser(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) {
@ -147,50 +135,38 @@ func (c *Commands) DeleteSchemaUser(ctx context.Context, resourceOwner, id strin
if err != nil {
return nil, err
}
if !writeModel.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-syHyCsGmvM", "Errors.User.NotFound")
}
if err := c.checkPermissionDeleteUser(ctx, writeModel.ResourceOwner, writeModel.AggregateID); err != nil {
events, err := writeModel.NewDelete(ctx)
if err != nil {
return nil, err
}
if err := c.pushAppendAndReduce(ctx, writeModel,
schemauser.NewDeletedEvent(ctx, UserV3AggregateFromWriteModel(&writeModel.WriteModel)),
); err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}
type ChangeSchemaUser struct {
Details *domain.ObjectDetails
SchemaID *string
schemaWriteModel *UserSchemaWriteModel
ResourceOwner string
ID string
Data json.RawMessage
SchemaUser *SchemaUser
Email *Email
ReturnCodeEmail string
ReturnCodeEmail *string
Phone *Phone
ReturnCodePhone string
ReturnCodePhone *string
}
func (s *ChangeSchemaUser) Valid(ctx context.Context, c *Commands) (err error) {
type SchemaUser struct {
SchemaID string
Data json.RawMessage
}
func (s *ChangeSchemaUser) Valid() (err error) {
if s.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-gEJR1QOGHb", "Errors.IDMissing")
}
if s.SchemaID != nil {
s.schemaWriteModel, err = c.getSchemaWriteModelByID(ctx, "", *s.SchemaID)
if err != nil {
return err
}
if !s.schemaWriteModel.Exists() {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-VLDTtxT3If", "Errors.UserSchema.NotExists")
}
}
if s.Email != nil && s.Email.Address != "" {
if err := s.Email.Validate(); err != nil {
return err
@ -206,92 +182,56 @@ func (s *ChangeSchemaUser) Valid(ctx context.Context, c *Commands) (err error) {
return nil
}
func (s *ChangeSchemaUser) ValidData(ctx context.Context, c *Commands, existingUser *UserV3WriteModel) (err error) {
// get role for permission check in schema through extension
role, err := c.getSchemaRoleForWrite(ctx, existingUser.ResourceOwner, existingUser.AggregateID)
if err != nil {
return err
}
if s.schemaWriteModel == nil {
s.schemaWriteModel, err = c.getSchemaWriteModelByID(ctx, "", existingUser.SchemaID)
if err != nil {
return err
}
}
schema, err := domain_schema.NewSchema(role, bytes.NewReader(s.schemaWriteModel.Schema))
if err != nil {
return err
}
// if data not changed but a new schema or revision should be used
data := s.Data
if s.Data == nil {
data = existingUser.Data
}
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-7o3ZGxtXUz", "Errors.User.Invalid")
}
if err := schema.Validate(v); err != nil {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")
}
return nil
}
func (c *Commands) ChangeSchemaUser(ctx context.Context, user *ChangeSchemaUser, alg crypto.EncryptionAlgorithm) (err error) {
if err := user.Valid(ctx, c); err != nil {
return err
func (c *Commands) ChangeSchemaUser(ctx context.Context, user *ChangeSchemaUser) (*domain.ObjectDetails, error) {
if err := user.Valid(); err != nil {
return nil, err
}
writeModel, err := c.getSchemaUserWriteModelByID(ctx, user.ResourceOwner, user.ID)
if err != nil {
return err
return nil, err
}
if !writeModel.Exists() {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.NotFound")
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.NotFound")
}
userAgg := UserV3AggregateFromWriteModel(&writeModel.WriteModel)
events := make([]eventstore.Command, 0)
if user.Data != nil || user.SchemaID != nil {
if err := user.ValidData(ctx, c, writeModel); err != nil {
return err
}
updateEvent := writeModel.NewUpdatedEvent(ctx,
userAgg,
user.schemaWriteModel.AggregateID,
user.schemaWriteModel.SchemaRevision,
user.Data,
)
if updateEvent != nil {
events = append(events, updateEvent)
}
schemaID := writeModel.SchemaID
if user.SchemaUser != nil && user.SchemaUser.SchemaID != "" {
schemaID = user.SchemaUser.SchemaID
}
if user.Email != nil {
events, user.ReturnCodeEmail, err = c.updateSchemaUserEmail(ctx, writeModel, events, userAgg, user.Email, alg)
var schemaWM *UserSchemaWriteModel
if user.SchemaUser != nil {
schemaWriteModel, err := c.getSchemaWriteModelByID(ctx, "", schemaID)
if err != nil {
return err
return nil, err
}
}
if user.Phone != nil {
events, user.ReturnCodePhone, err = c.updateSchemaUserPhone(ctx, writeModel, events, userAgg, user.Phone, alg)
if err != nil {
return err
if !schemaWriteModel.Exists() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-VLDTtxT3If", "Errors.UserSchema.NotExists")
}
schemaWM = schemaWriteModel
}
if len(events) == 0 {
user.Details = writeModelToObjectDetails(&writeModel.WriteModel)
return nil
events, codeEmail, codePhone, err := writeModel.NewUpdate(ctx,
schemaWM,
user.SchemaUser,
user.Email,
user.Phone,
func(ctx context.Context) (*EncryptedCode, error) {
return c.newEmailCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck
},
)
if err != nil {
return nil, err
}
if err := c.pushAppendAndReduce(ctx, writeModel, events...); err != nil {
return err
if codeEmail != "" {
user.ReturnCodeEmail = &codeEmail
}
user.Details = writeModelToObjectDetails(&writeModel.WriteModel)
return nil
if codePhone != "" {
user.ReturnCodePhone = &codePhone
}
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}
func (c *Commands) checkPermissionUpdateUserState(ctx context.Context, resourceOwner, userID string) error {
@ -368,7 +308,7 @@ func (c *Commands) ActivateSchemaUser(ctx context.Context, resourceOwner, id str
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-17XupGvxBJ", "Errors.IDMissing")
}
writeModel, err := c.getSchemaUserExists(ctx, "", id)
writeModel, err := c.getSchemaUserExists(ctx, resourceOwner, id)
if err != nil {
return nil, err
}
@ -386,65 +326,8 @@ func (c *Commands) ActivateSchemaUser(ctx context.Context, resourceOwner, id str
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) updateSchemaUserEmail(ctx context.Context, existing *UserV3WriteModel, events []eventstore.Command, agg *eventstore.Aggregate, email *Email, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) {
if existing.Email == string(email.Address) {
return events, plainCode, nil
}
events = append(events, schemauser.NewEmailUpdatedEvent(ctx,
agg,
email.Address,
))
if email.Verified {
events = append(events, schemauser.NewEmailVerifiedEvent(ctx, agg))
} else {
cryptoCode, err := c.newEmailCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck
if err != nil {
return nil, "", err
}
if email.ReturnCode {
plainCode = cryptoCode.Plain
}
events = append(events, schemauser.NewEmailCodeAddedEvent(ctx, agg,
cryptoCode.Crypted,
cryptoCode.Expiry,
email.URLTemplate,
email.ReturnCode,
))
}
return events, plainCode, nil
}
func (c *Commands) updateSchemaUserPhone(ctx context.Context, existing *UserV3WriteModel, events []eventstore.Command, agg *eventstore.Aggregate, phone *Phone, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) {
if existing.Phone == string(phone.Number) {
return events, plainCode, nil
}
events = append(events, schemauser.NewPhoneUpdatedEvent(ctx,
agg,
phone.Number,
))
if phone.Verified {
events = append(events, schemauser.NewPhoneVerifiedEvent(ctx, agg))
} else {
cryptoCode, err := c.newPhoneCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck
if err != nil {
return nil, "", err
}
if phone.ReturnCode {
plainCode = cryptoCode.Plain
}
events = append(events, schemauser.NewPhoneCodeAddedEvent(ctx, agg,
cryptoCode.Crypted,
cryptoCode.Expiry,
phone.ReturnCode,
))
}
return events, plainCode, nil
}
func (c *Commands) getSchemaUserExists(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) {
writeModel := NewExistsUserV3WriteModel(resourceOwner, id)
writeModel := NewExistsUserV3WriteModel(resourceOwner, id, c.checkPermission)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
@ -452,7 +335,23 @@ func (c *Commands) getSchemaUserExists(ctx context.Context, resourceOwner, id st
}
func (c *Commands) getSchemaUserWriteModelByID(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) {
writeModel := NewUserV3WriteModel(resourceOwner, id)
writeModel := NewUserV3WriteModel(resourceOwner, id, c.checkPermission)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
return writeModel, nil
}
func (c *Commands) getSchemaUserEmailWriteModelByID(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) {
writeModel := NewUserV3EmailWriteModel(resourceOwner, id, c.checkPermission)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
return writeModel, nil
}
func (c *Commands) getSchemaUserPhoneWriteModelByID(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) {
writeModel := NewUserV3PhoneWriteModel(resourceOwner, id, c.checkPermission)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}

View File

@ -0,0 +1,115 @@
package command
import (
"context"
"io"
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
)
type ChangeSchemaUserEmail struct {
ResourceOwner string
ID string
Email *Email
ReturnCode *string
}
func (s *ChangeSchemaUserEmail) Valid() (err error) {
if s.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-0oj2PquNGA", "Errors.IDMissing")
}
if s.Email != nil && s.Email.Address != "" {
if err := s.Email.Validate(); err != nil {
return err
}
}
if s.Email != nil && s.Email.URLTemplate != "" {
if err := domain.RenderConfirmURLTemplate(io.Discard, s.Email.URLTemplate, s.ID, "code", "orgID"); err != nil {
return err
}
}
return nil
}
func (c *Commands) ChangeSchemaUserEmail(ctx context.Context, user *ChangeSchemaUserEmail) (_ *domain.ObjectDetails, err error) {
if err := user.Valid(); err != nil {
return nil, err
}
writeModel, err := c.getSchemaUserEmailWriteModelByID(ctx, user.ResourceOwner, user.ID)
if err != nil {
return nil, err
}
events, plainCode, err := writeModel.NewEmailUpdate(ctx,
user.Email,
func(ctx context.Context) (*EncryptedCode, error) {
return c.newEmailCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck
},
)
if err != nil {
return nil, err
}
if plainCode != "" {
user.ReturnCode = &plainCode
}
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}
func (c *Commands) VerifySchemaUserEmail(ctx context.Context, resourceOwner, id, code string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-y3n4Sdu8j5", "Errors.IDMissing")
}
writeModel, err := c.getSchemaUserEmailWriteModelByID(ctx, resourceOwner, id)
if err != nil {
return nil, err
}
events, err := writeModel.NewEmailVerify(ctx,
func(creationDate time.Time, expiry time.Duration, cryptoCode *crypto.CryptoValue) error {
return crypto.VerifyCode(creationDate, expiry, cryptoCode, code, c.userEncryption)
},
)
if err != nil {
return nil, err
}
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}
type ResendSchemaUserEmailCode struct {
ResourceOwner string
ID string
URLTemplate string
ReturnCode bool
PlainCode *string
}
func (c *Commands) ResendSchemaUserEmailCode(ctx context.Context, user *ResendSchemaUserEmailCode) (*domain.ObjectDetails, error) {
if user.ID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-KvPc5o9GeJ", "Errors.IDMissing")
}
writeModel, err := c.getSchemaUserEmailWriteModelByID(ctx, user.ResourceOwner, user.ID)
if err != nil {
return nil, err
}
events, plainCode, err := writeModel.NewResendEmailCode(ctx,
func(ctx context.Context) (*EncryptedCode, error) {
return c.newEmailCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck
},
user.URLTemplate,
user.ReturnCode,
)
if err != nil {
return nil, err
}
if plainCode != "" {
user.PlainCode = &plainCode
}
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}

File diff suppressed because it is too large Load Diff

View File

@ -4,10 +4,15 @@ import (
"bytes"
"context"
"encoding/json"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
domain_schema "github.com/zitadel/zitadel/internal/domain/schema"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user/schemauser"
"github.com/zitadel/zitadel/internal/zerrors"
)
type UserV3WriteModel struct {
@ -23,37 +28,77 @@ type UserV3WriteModel struct {
Email string
IsEmailVerified bool
EmailVerifiedFailedCount int
EmailCode *VerifyCode
Phone string
IsPhoneVerified bool
PhoneVerifiedFailedCount int
PhoneCode *VerifyCode
Data json.RawMessage
Locked bool
State domain.UserState
checkPermission domain.PermissionCheck
writePermissionCheck bool
}
func NewExistsUserV3WriteModel(resourceOwner, userID string) *UserV3WriteModel {
func (wm *UserV3WriteModel) GetWriteModel() *eventstore.WriteModel {
return &wm.WriteModel
}
type VerifyCode struct {
Code *crypto.CryptoValue
CreationDate time.Time
Expiry time.Duration
}
func NewExistsUserV3WriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
return &UserV3WriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
PhoneWM: false,
EmailWM: false,
DataWM: false,
PhoneWM: false,
EmailWM: false,
DataWM: false,
checkPermission: checkPermission,
}
}
func NewUserV3WriteModel(resourceOwner, userID string) *UserV3WriteModel {
func NewUserV3WriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
return &UserV3WriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
PhoneWM: true,
EmailWM: true,
DataWM: true,
PhoneWM: true,
EmailWM: true,
DataWM: true,
checkPermission: checkPermission,
}
}
func NewUserV3EmailWriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
return &UserV3WriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
EmailWM: true,
checkPermission: checkPermission,
}
}
func NewUserV3PhoneWriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
return &UserV3WriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
PhoneWM: true,
checkPermission: checkPermission,
}
}
@ -83,24 +128,38 @@ func (wm *UserV3WriteModel) Reduce() error {
wm.Email = string(e.EmailAddress)
wm.IsEmailVerified = false
wm.EmailVerifiedFailedCount = 0
wm.EmailCode = nil
case *schemauser.EmailCodeAddedEvent:
wm.IsEmailVerified = false
wm.EmailVerifiedFailedCount = 0
wm.EmailCode = &VerifyCode{
Code: e.Code,
CreationDate: e.CreationDate(),
Expiry: e.Expiry,
}
case *schemauser.EmailVerifiedEvent:
wm.IsEmailVerified = true
wm.EmailVerifiedFailedCount = 0
wm.EmailCode = nil
case *schemauser.EmailVerificationFailedEvent:
wm.EmailVerifiedFailedCount += 1
case *schemauser.PhoneUpdatedEvent:
wm.Phone = string(e.PhoneNumber)
wm.IsPhoneVerified = false
wm.PhoneVerifiedFailedCount = 0
wm.EmailCode = nil
case *schemauser.PhoneCodeAddedEvent:
wm.IsPhoneVerified = false
wm.PhoneVerifiedFailedCount = 0
wm.PhoneCode = &VerifyCode{
Code: e.Code,
CreationDate: e.CreationDate(),
Expiry: e.Expiry,
}
case *schemauser.PhoneVerifiedEvent:
wm.PhoneVerifiedFailedCount = 0
wm.IsPhoneVerified = true
wm.PhoneCode = nil
case *schemauser.PhoneVerificationFailedEvent:
wm.PhoneVerifiedFailedCount += 1
case *schemauser.LockedEvent:
@ -156,13 +215,159 @@ func (wm *UserV3WriteModel) Query() *eventstore.SearchQueryBuilder {
EventTypes(eventtypes...).Builder()
}
func (wm *UserV3WriteModel) NewUpdatedEvent(
func (wm *UserV3WriteModel) NewCreated(
ctx context.Context,
agg *eventstore.Aggregate,
schemaID string,
schemaRevision uint64,
data json.RawMessage,
) *schemauser.UpdatedEvent {
email *Email,
phone *Phone,
code func(context.Context) (*EncryptedCode, error),
) (_ []eventstore.Command, codeEmail string, codePhone string, err error) {
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", "", err
}
if wm.Exists() {
return nil, "", "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.AlreadyExists")
}
events := []eventstore.Command{
schemauser.NewCreatedEvent(ctx,
UserV3AggregateFromWriteModel(&wm.WriteModel),
schemaID, schemaRevision, data,
),
}
if email != nil {
emailEvents, plainCodeEmail, err := wm.NewEmailCreate(ctx,
email,
code,
)
if err != nil {
return nil, "", "", err
}
if plainCodeEmail != "" {
codeEmail = plainCodeEmail
}
events = append(events, emailEvents...)
}
if phone != nil {
phoneEvents, plainCodePhone, err := wm.NewPhoneCreate(ctx,
phone,
code,
)
if err != nil {
return nil, "", "", err
}
if plainCodePhone != "" {
codePhone = plainCodePhone
}
events = append(events, phoneEvents...)
}
return events, codeEmail, codePhone, nil
}
func (wm *UserV3WriteModel) getSchemaRoleForWrite(ctx context.Context, resourceOwner, userID string) (domain_schema.Role, error) {
if userID == authz.GetCtxData(ctx).UserID {
return domain_schema.RoleSelf, nil
}
if err := wm.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil {
return domain_schema.RoleUnspecified, err
}
return domain_schema.RoleOwner, nil
}
func (wm *UserV3WriteModel) validateData(ctx context.Context, data []byte, schemaWM *UserSchemaWriteModel) (string, uint64, error) {
// get role for permission check in schema through extension
role, err := wm.getSchemaRoleForWrite(ctx, wm.ResourceOwner, wm.AggregateID)
if err != nil {
return "", 0, err
}
schema, err := domain_schema.NewSchema(role, bytes.NewReader(schemaWM.Schema))
if err != nil {
return "", 0, err
}
// if data not changed but a new schema or revision should be used
if data == nil {
data = wm.Data
}
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return "", 0, zerrors.ThrowInvalidArgument(nil, "COMMAND-7o3ZGxtXUz", "Errors.User.Invalid")
}
if err := schema.Validate(v); err != nil {
return "", 0, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")
}
return schemaWM.AggregateID, schemaWM.SchemaRevision, nil
}
func (wm *UserV3WriteModel) NewUpdate(
ctx context.Context,
schemaWM *UserSchemaWriteModel,
user *SchemaUser,
email *Email,
phone *Phone,
code func(context.Context) (*EncryptedCode, error),
) (_ []eventstore.Command, codeEmail string, codePhone string, err error) {
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", "", err
}
if !wm.Exists() {
return nil, "", "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.NotFound")
}
events := make([]eventstore.Command, 0)
if user != nil {
schemaID, schemaRevision, err := wm.validateData(ctx, user.Data, schemaWM)
if err != nil {
return nil, "", "", err
}
userEvents := wm.newUpdatedEvents(ctx,
schemaID,
schemaRevision,
user.Data,
)
events = append(events, userEvents...)
}
if email != nil {
emailEvents, plainCodeEmail, err := wm.NewEmailUpdate(ctx,
email,
code,
)
if err != nil {
return nil, "", "", err
}
if plainCodeEmail != "" {
codeEmail = plainCodeEmail
}
events = append(events, emailEvents...)
}
if phone != nil {
phoneEvents, plainCodePhone, err := wm.NewPhoneCreate(ctx,
phone,
code,
)
if err != nil {
return nil, "", "", err
}
if plainCodePhone != "" {
codePhone = plainCodePhone
}
events = append(events, phoneEvents...)
}
return events, codeEmail, codePhone, nil
}
func (wm *UserV3WriteModel) newUpdatedEvents(
ctx context.Context,
schemaID string,
schemaRevision uint64,
data json.RawMessage,
) []eventstore.Command {
changes := make([]schemauser.Changes, 0)
if wm.SchemaID != schemaID {
changes = append(changes, schemauser.ChangeSchemaID(schemaID))
@ -176,7 +381,20 @@ func (wm *UserV3WriteModel) NewUpdatedEvent(
if len(changes) == 0 {
return nil
}
return schemauser.NewUpdatedEvent(ctx, agg, changes)
return []eventstore.Command{schemauser.NewUpdatedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel), changes)}
}
func (wm *UserV3WriteModel) NewDelete(
ctx context.Context,
) (_ []eventstore.Command, err error) {
if !wm.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-syHyCsGmvM", "Errors.User.NotFound")
}
if err := wm.checkPermissionDelete(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, err
}
return []eventstore.Command{schemauser.NewDeletedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel))}, nil
}
func UserV3AggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
@ -192,3 +410,271 @@ func UserV3AggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggreg
func (wm *UserV3WriteModel) Exists() bool {
return wm.State != domain.UserStateDeleted && wm.State != domain.UserStateUnspecified
}
func (wm *UserV3WriteModel) checkPermissionWrite(
ctx context.Context,
resourceOwner string,
userID string,
) error {
if wm.writePermissionCheck {
return nil
}
if userID != "" && userID == authz.GetCtxData(ctx).UserID {
return nil
}
if err := wm.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil {
return err
}
wm.writePermissionCheck = true
return nil
}
func (wm *UserV3WriteModel) checkPermissionDelete(
ctx context.Context,
resourceOwner string,
userID string,
) error {
if userID != "" && userID == authz.GetCtxData(ctx).UserID {
return nil
}
return wm.checkPermission(ctx, domain.PermissionUserDelete, resourceOwner, userID)
}
func (wm *UserV3WriteModel) NewEmailCreate(
ctx context.Context,
email *Email,
code func(context.Context) (*EncryptedCode, error),
) (_ []eventstore.Command, plainCode string, err error) {
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", err
}
if email == nil || wm.Email == string(email.Address) {
return nil, "", nil
}
events := []eventstore.Command{
schemauser.NewEmailUpdatedEvent(ctx,
UserV3AggregateFromWriteModel(&wm.WriteModel),
email.Address,
),
}
if email.Verified {
events = append(events, wm.newEmailVerifiedEvent(ctx))
} else {
codeEvent, code, err := wm.newEmailCodeAddedEvent(ctx, code, email.URLTemplate, email.ReturnCode)
if err != nil {
return nil, "", err
}
events = append(events, codeEvent)
if code != "" {
plainCode = code
}
}
return events, plainCode, nil
}
func (wm *UserV3WriteModel) NewEmailUpdate(
ctx context.Context,
email *Email,
code func(context.Context) (*EncryptedCode, error),
) (_ []eventstore.Command, plainCode string, err error) {
if !wm.EmailWM {
return nil, "", nil
}
if !wm.Exists() {
return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-nJ0TQFuRmP", "Errors.User.NotFound")
}
return wm.NewEmailCreate(ctx, email, code)
}
func (wm *UserV3WriteModel) NewEmailVerify(
ctx context.Context,
verify func(creationDate time.Time, expiry time.Duration, cryptoCode *crypto.CryptoValue) error,
) ([]eventstore.Command, error) {
if !wm.EmailWM {
return nil, nil
}
if !wm.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-qbGyMPvjvj", "Errors.User.NotFound")
}
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, err
}
if wm.EmailCode == nil {
return nil, nil
}
if err := verify(wm.EmailCode.CreationDate, wm.EmailCode.Expiry, wm.EmailCode.Code); err != nil {
return nil, err
}
return []eventstore.Command{wm.newEmailVerifiedEvent(ctx)}, nil
}
func (wm *UserV3WriteModel) newEmailVerifiedEvent(
ctx context.Context,
) *schemauser.EmailVerifiedEvent {
return schemauser.NewEmailVerifiedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel))
}
func (wm *UserV3WriteModel) NewResendEmailCode(
ctx context.Context,
code func(context.Context) (*EncryptedCode, error),
urlTemplate string,
isReturnCode bool,
) (_ []eventstore.Command, plainCode string, err error) {
if !wm.EmailWM {
return nil, "", nil
}
if !wm.Exists() {
return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-EajeF6ypOV", "Errors.User.NotFound")
}
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", err
}
if wm.EmailCode == nil {
return nil, "", zerrors.ThrowPreconditionFailed(err, "COMMAND-QRkNTBwF8q", "Errors.User.Code.Empty")
}
event, plainCode, err := wm.newEmailCodeAddedEvent(ctx, code, urlTemplate, isReturnCode)
if err != nil {
return nil, "", err
}
return []eventstore.Command{event}, plainCode, nil
}
func (wm *UserV3WriteModel) newEmailCodeAddedEvent(
ctx context.Context,
code func(context.Context) (*EncryptedCode, error),
urlTemplate string,
isReturnCode bool,
) (_ *schemauser.EmailCodeAddedEvent, plainCode string, err error) {
cryptoCode, err := code(ctx)
if err != nil {
return nil, "", err
}
if isReturnCode {
plainCode = cryptoCode.Plain
}
return schemauser.NewEmailCodeAddedEvent(ctx,
UserV3AggregateFromWriteModel(&wm.WriteModel),
cryptoCode.Crypted,
cryptoCode.Expiry,
urlTemplate,
isReturnCode,
), plainCode, nil
}
func (wm *UserV3WriteModel) NewPhoneCreate(
ctx context.Context,
phone *Phone,
code func(context.Context) (*EncryptedCode, error),
) (_ []eventstore.Command, plainCode string, err error) {
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", err
}
if phone == nil || wm.Phone == string(phone.Number) {
return nil, "", nil
}
events := []eventstore.Command{
schemauser.NewPhoneUpdatedEvent(ctx,
UserV3AggregateFromWriteModel(&wm.WriteModel),
phone.Number,
),
}
if phone.Verified {
events = append(events, wm.newPhoneVerifiedEvent(ctx))
} else {
codeEvent, code, err := wm.newPhoneCodeAddedEvent(ctx, code, phone.ReturnCode)
if err != nil {
return nil, "", err
}
events = append(events, codeEvent)
if code != "" {
plainCode = code
}
}
return events, plainCode, nil
}
func (wm *UserV3WriteModel) NewPhoneUpdate(
ctx context.Context,
phone *Phone,
code func(context.Context) (*EncryptedCode, error),
) (_ []eventstore.Command, plainCode string, err error) {
if !wm.PhoneWM {
return nil, "", nil
}
if !wm.Exists() {
return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-b33QAVgel6", "Errors.User.NotFound")
}
return wm.NewPhoneCreate(ctx, phone, code)
}
func (wm *UserV3WriteModel) NewPhoneVerify(
ctx context.Context,
verify func(creationDate time.Time, expiry time.Duration, cryptoCode *crypto.CryptoValue) error,
) ([]eventstore.Command, error) {
if !wm.PhoneWM {
return nil, nil
}
if !wm.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-bx2OLtgGNS", "Errors.User.NotFound")
}
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, err
}
if wm.PhoneCode == nil {
return nil, nil
}
if err := verify(wm.PhoneCode.CreationDate, wm.PhoneCode.Expiry, wm.PhoneCode.Code); err != nil {
return nil, err
}
return []eventstore.Command{wm.newPhoneVerifiedEvent(ctx)}, nil
}
func (wm *UserV3WriteModel) newPhoneVerifiedEvent(
ctx context.Context,
) *schemauser.PhoneVerifiedEvent {
return schemauser.NewPhoneVerifiedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel))
}
func (wm *UserV3WriteModel) NewResendPhoneCode(
ctx context.Context,
code func(context.Context) (*EncryptedCode, error),
isReturnCode bool,
) (_ []eventstore.Command, plainCode string, err error) {
if !wm.PhoneWM {
return nil, "", nil
}
if !wm.Exists() {
return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-z8Bu9vuL9s", "Errors.User.NotFound")
}
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", err
}
if wm.PhoneCode == nil {
return nil, "", zerrors.ThrowPreconditionFailed(err, "COMMAND-fEsHdqECzb", "Errors.User.Code.Empty")
}
event, plainCode, err := wm.newPhoneCodeAddedEvent(ctx, code, isReturnCode)
if err != nil {
return nil, "", err
}
return []eventstore.Command{event}, plainCode, nil
}
func (wm *UserV3WriteModel) newPhoneCodeAddedEvent(
ctx context.Context,
code func(context.Context) (*EncryptedCode, error),
isReturnCode bool,
) (_ *schemauser.PhoneCodeAddedEvent, plainCode string, err error) {
cryptoCode, err := code(ctx)
if err != nil {
return nil, "", err
}
if isReturnCode {
plainCode = cryptoCode.Plain
}
return schemauser.NewPhoneCodeAddedEvent(ctx,
UserV3AggregateFromWriteModel(&wm.WriteModel),
cryptoCode.Crypted,
cryptoCode.Expiry,
isReturnCode,
), plainCode, nil
}

View File

@ -0,0 +1,107 @@
package command
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
)
type ChangeSchemaUserPhone struct {
ResourceOwner string
ID string
Phone *Phone
ReturnCode *string
}
func (s *ChangeSchemaUserPhone) Valid() (err error) {
if s.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-DkQ9aurv5u", "Errors.IDMissing")
}
if s.Phone != nil && s.Phone.Number != "" {
if s.Phone.Number, err = s.Phone.Number.Normalize(); err != nil {
return err
}
}
return nil
}
func (c *Commands) ChangeSchemaUserPhone(ctx context.Context, user *ChangeSchemaUserPhone) (_ *domain.ObjectDetails, err error) {
if err := user.Valid(); err != nil {
return nil, err
}
writeModel, err := c.getSchemaUserPhoneWriteModelByID(ctx, user.ResourceOwner, user.ID)
if err != nil {
return nil, err
}
events, plainCode, err := writeModel.NewPhoneUpdate(ctx,
user.Phone,
func(ctx context.Context) (*EncryptedCode, error) {
return c.newPhoneCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck
},
)
if err != nil {
return nil, err
}
if plainCode != "" {
user.ReturnCode = &plainCode
}
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}
func (c *Commands) VerifySchemaUserPhone(ctx context.Context, resourceOwner, id, code string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-R4LKY44Ke3", "Errors.IDMissing")
}
writeModel, err := c.getSchemaUserPhoneWriteModelByID(ctx, resourceOwner, id)
if err != nil {
return nil, err
}
events, err := writeModel.NewPhoneVerify(ctx,
func(creationDate time.Time, expiry time.Duration, cryptoCode *crypto.CryptoValue) error {
return crypto.VerifyCode(creationDate, expiry, cryptoCode, code, c.userEncryption)
},
)
if err != nil {
return nil, err
}
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}
type ResendSchemaUserPhoneCode struct {
ResourceOwner string
ID string
ReturnCode bool
PlainCode *string
}
func (c *Commands) ResendSchemaUserPhoneCode(ctx context.Context, user *ResendSchemaUserPhoneCode) (*domain.ObjectDetails, error) {
if user.ID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-zmxIFR2nMo", "Errors.IDMissing")
}
writeModel, err := c.getSchemaUserPhoneWriteModelByID(ctx, user.ResourceOwner, user.ID)
if err != nil {
return nil, err
}
events, plainCode, err := writeModel.NewResendPhoneCode(ctx,
func(ctx context.Context) (*EncryptedCode, error) {
return c.newPhoneCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck
},
user.ReturnCode,
)
if err != nil {
return nil, err
}
if plainCode != "" {
user.PlainCode = &plainCode
}
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@ import (
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
@ -875,8 +874,9 @@ func TestCommands_CreateSchemaUser(t *testing.T) {
idGenerator: tt.fields.idGenerator,
checkPermission: tt.fields.checkPermission,
newEncryptedCode: tt.fields.newCode,
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
}
err := c.CreateSchemaUser(tt.args.ctx, tt.args.user, crypto.CreateMockEncryptionAlg(gomock.NewController(t)))
details, err := c.CreateSchemaUser(tt.args.ctx, tt.args.user)
if tt.res.err == nil {
assert.NoError(t, err)
}
@ -884,14 +884,16 @@ func TestCommands_CreateSchemaUser(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assertObjectDetails(t, tt.res.details, tt.args.user.Details)
assertObjectDetails(t, tt.res.details, details)
}
if tt.res.returnCodePhone != "" {
assert.Equal(t, tt.res.returnCodePhone, tt.args.user.ReturnCodePhone)
assert.NotNil(t, tt.args.user.ReturnCodePhone)
assert.Equal(t, tt.res.returnCodePhone, *tt.args.user.ReturnCodePhone)
}
if tt.res.returnCodeEmail != "" {
assert.Equal(t, tt.res.returnCodeEmail, tt.args.user.ReturnCodeEmail)
assert.NotNil(t, tt.args.user.ReturnCodeEmail)
assert.Equal(t, tt.res.returnCodeEmail, *tt.args.user.ReturnCodeEmail)
}
})
}
@ -1988,6 +1990,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
"schema not existing, error",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
@ -1995,8 +2010,10 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
args{
ctx: authz.NewMockContext("instanceID", "", ""),
user: &ChangeSchemaUser{
ID: "user1",
SchemaID: gu.Ptr("type"),
ID: "user1",
SchemaUser: &SchemaUser{
SchemaID: "type",
},
},
},
res{
@ -2060,6 +2077,25 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
@ -2067,9 +2103,11 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
ctx: authz.NewMockContext("instanceID", "", ""),
user: &ChangeSchemaUser{
ID: "user1",
Data: json.RawMessage(`{
SchemaUser: &SchemaUser{
Data: json.RawMessage(`{
"name": "user"
}`),
},
},
},
res{
@ -2134,9 +2172,11 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
ctx: authz.NewMockContext("instanceID", "", ""),
user: &ChangeSchemaUser{
ID: "user1",
Data: json.RawMessage(`{
SchemaUser: &SchemaUser{
Data: json.RawMessage(`{
"name": "user2"
}`),
},
},
},
res{
@ -2149,6 +2189,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
"user updated, changed schema",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
@ -2168,19 +2221,6 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectPush(
schemauser.NewUpdatedEvent(
context.Background(),
@ -2196,8 +2236,10 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
args{
ctx: authz.NewMockContext("instanceID", "", ""),
user: &ChangeSchemaUser{
ID: "user1",
SchemaID: gu.Ptr("id2"),
ID: "user1",
SchemaUser: &SchemaUser{
SchemaID: "id2",
},
},
},
res{
@ -2210,6 +2252,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
"user updated, new schema",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
@ -2229,19 +2284,6 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectPush(
schemauser.NewUpdatedEvent(
context.Background(),
@ -2262,11 +2304,13 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
args{
ctx: authz.NewMockContext("instanceID", "", ""),
user: &ChangeSchemaUser{
ID: "user1",
SchemaID: gu.Ptr("id2"),
Data: json.RawMessage(`{
ID: "user1",
SchemaUser: &SchemaUser{
SchemaID: "id2",
Data: json.RawMessage(`{
"name": "user2"
}`),
},
},
},
res{
@ -2350,9 +2394,11 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
ctx: authz.NewMockContext("instanceID", "", ""),
user: &ChangeSchemaUser{
ID: "user1",
Data: json.RawMessage(`{
SchemaUser: &SchemaUser{
Data: json.RawMessage(`{
"name2": "user2"
}`),
},
},
},
res{
@ -2365,6 +2411,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
"user updated, new schema and revision",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
2,
json.RawMessage(`{
"name1": "user1"
}`),
),
),
),
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
@ -2384,19 +2443,6 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
2,
json.RawMessage(`{
"name1": "user1"
}`),
),
),
),
expectPush(
schemauser.NewUpdatedEvent(
context.Background(),
@ -2418,11 +2464,13 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
args{
ctx: authz.NewMockContext("instanceID", "", ""),
user: &ChangeSchemaUser{
ID: "user1",
SchemaID: gu.Ptr("id2"),
Data: json.RawMessage(`{
ID: "user1",
SchemaUser: &SchemaUser{
SchemaID: "id2",
Data: json.RawMessage(`{
"name2": "user2"
}`),
},
},
},
res{
@ -2435,6 +2483,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
"user update, no field permission as admin",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
@ -2457,30 +2518,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
user: &ChangeSchemaUser{
ID: "user1",
SchemaID: gu.Ptr("id1"),
Data: json.RawMessage(`{
ID: "user1",
SchemaUser: &SchemaUser{
SchemaID: "id1",
Data: json.RawMessage(`{
"name": "user"
}`),
},
},
},
res{
@ -2494,6 +2544,18 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
), expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
@ -2515,29 +2577,18 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "org1", "user1"),
user: &ChangeSchemaUser{
ID: "user1",
SchemaID: gu.Ptr("type"),
Data: json.RawMessage(`{
ID: "user1",
SchemaUser: &SchemaUser{
SchemaID: "type",
Data: json.RawMessage(`{
"name": "user"
}`),
},
},
},
res{
@ -2550,6 +2601,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
"user update, invalid data type",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
@ -2569,30 +2633,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args{
ctx: authz.NewMockContext("instanceID", "org1", "user1"),
user: &ChangeSchemaUser{
ID: "user1",
SchemaID: gu.Ptr("type"),
Data: json.RawMessage(`{
ID: "user1",
SchemaUser: &SchemaUser{
SchemaID: "type",
Data: json.RawMessage(`{
"name": 1
}`),
},
},
},
res{
@ -2605,6 +2658,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
"user update, additional property",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
@ -2624,19 +2690,6 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectPush(
schemauser.NewUpdatedEvent(
context.Background(),
@ -2657,12 +2710,14 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
args{
ctx: authz.NewMockContext("instanceID", "", ""),
user: &ChangeSchemaUser{
ID: "user1",
SchemaID: gu.Ptr("id1"),
Data: json.RawMessage(`{
ID: "user1",
SchemaUser: &SchemaUser{
SchemaID: "id1",
Data: json.RawMessage(`{
"name": "user1",
"additional": "property"
}`),
},
},
},
res{
@ -2675,6 +2730,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
"user update, invalid data attribute name",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
@ -2695,30 +2763,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args{
ctx: authz.NewMockContext("instanceID", "org1", "user1"),
user: &ChangeSchemaUser{
ID: "user1",
SchemaID: gu.Ptr("type"),
Data: json.RawMessage(`{
ID: "user1",
SchemaUser: &SchemaUser{
SchemaID: "type",
Data: json.RawMessage(`{
"invalid": "user"
}`),
},
},
},
res{
@ -2775,6 +2832,19 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
"user update, email return",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
@ -2794,19 +2864,6 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
schemauser.NewCreatedEvent(
context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
"id1",
1,
json.RawMessage(`{
"name": "user1"
}`),
),
),
),
expectPush(
schemauser.NewEmailUpdatedEvent(context.Background(),
&schemauser.NewAggregate("user1", "org1").Aggregate,
@ -2832,8 +2889,10 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
args{
ctx: authz.NewMockContext("instanceID", "", ""),
user: &ChangeSchemaUser{
ID: "user1",
SchemaID: gu.Ptr("id1"),
ID: "user1",
SchemaUser: &SchemaUser{
SchemaID: "id1",
},
Email: &Email{
Address: "test@example.com",
ReturnCode: true,
@ -3101,8 +3160,9 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
newEncryptedCode: tt.fields.newCode,
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
}
err := c.ChangeSchemaUser(tt.args.ctx, tt.args.user, crypto.CreateMockEncryptionAlg(gomock.NewController(t)))
details, err := c.ChangeSchemaUser(tt.args.ctx, tt.args.user)
if tt.res.err == nil {
assert.NoError(t, err)
}
@ -3110,14 +3170,16 @@ func TestCommands_ChangeSchemaUser(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assertObjectDetails(t, tt.res.details, tt.args.user.Details)
assertObjectDetails(t, tt.res.details, details)
}
if tt.res.returnCodePhone != "" {
assert.Equal(t, tt.res.returnCodePhone, tt.args.user.ReturnCodePhone)
assert.NotNil(t, tt.args.user.ReturnCodePhone)
assert.Equal(t, tt.res.returnCodePhone, *tt.args.user.ReturnCodePhone)
}
if tt.res.returnCodeEmail != "" {
assert.Equal(t, tt.res.returnCodeEmail, tt.args.user.ReturnCodeEmail)
assert.NotNil(t, tt.args.user.ReturnCodeEmail)
assert.Equal(t, tt.res.returnCodeEmail, *tt.args.user.ReturnCodeEmail)
}
})
}

View File

@ -776,6 +776,32 @@ func (i *Instance) CreateSchemaUser(ctx context.Context, orgID string, schemaID
return user
}
func (i *Instance) UpdateSchemaUserEmail(ctx context.Context, orgID string, userID string, email string) *user_v3alpha.SetContactEmailResponse {
user, err := i.Client.UserV3Alpha.SetContactEmail(ctx, &user_v3alpha.SetContactEmailRequest{
Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}},
Id: userID,
Email: &user_v3alpha.SetEmail{
Address: email,
Verification: &user_v3alpha.SetEmail_ReturnCode{},
},
})
logging.OnError(err).Fatal("create user")
return user
}
func (i *Instance) UpdateSchemaUserPhone(ctx context.Context, orgID string, userID string, phone string) *user_v3alpha.SetContactPhoneResponse {
user, err := i.Client.UserV3Alpha.SetContactPhone(ctx, &user_v3alpha.SetContactPhoneRequest{
Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}},
Id: userID,
Phone: &user_v3alpha.SetPhone{
Number: phone,
Verification: &user_v3alpha.SetPhone_ReturnCode{},
},
})
logging.OnError(err).Fatal("create user")
return user
}
func (i *Instance) CreateInviteCode(ctx context.Context, userID string) *user_v2.CreateInviteCodeResponse {
user, err := i.Client.UserV2.CreateInviteCode(ctx, &user_v2.CreateInviteCodeRequest{
UserId: userID,

View File

@ -1392,7 +1392,7 @@ message SetContactPhoneRequest {
message SetContactPhoneResponse {
zitadel.resources.object.v3alpha.Details details = 1;
// The phone verification code will be set if a contact phone was set with a return_code verification option.
optional string email_code = 3 [
optional string verification_code = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"SKJd342k\"";
}