From 5fdad7b8f4218d925cf4d8c799c94d5697d216d5 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:27:48 +0200 Subject: [PATCH 01/16] feat: user v3 api update (#8582) # Which Problems Are Solved Users are not yet able to update their information an status in user API v3. # How the Problems Are Solved Add endpoints and functionality to update users and their status in user API v3. # Additional Changes Aggregate_type and event_types are updated with "userschema" to avoid conflicts with old events. # Additional Context closes #7898 --- .../integration_test/execution_target_test.go | 26 +- .../resources/object/v3alpha/converter.go | 15 + .../v3alpha/integration_test/server_test.go | 25 +- .../v3alpha/integration_test/user_test.go | 1344 ++++++++++- .../api/grpc/resources/user/v3alpha/user.go | 146 +- .../v3alpha/integration_test/query_test.go | 42 +- .../v3alpha/integration_test/server_test.go | 24 +- .../integration_test/userschema_test.go | 151 +- internal/command/user_schema.go | 2 +- internal/command/user_schema_model.go | 15 +- internal/command/user_v3.go | 264 ++- internal/command/user_v3_model.go | 66 +- internal/command/user_v3_test.go | 2029 ++++++++++++++++- internal/integration/client.go | 54 +- .../repository/user/schemauser/aggregate.go | 4 +- internal/repository/user/schemauser/email.go | 87 +- .../repository/user/schemauser/eventstore.go | 14 + internal/repository/user/schemauser/phone.go | 91 +- internal/repository/user/schemauser/user.go | 136 +- .../resources/user/v3alpha/user_service.proto | 142 +- 20 files changed, 4265 insertions(+), 412 deletions(-) diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go index 2bdbdee066..169ee0e5d2 100644 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go +++ b/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go @@ -250,16 +250,26 @@ func TestServer_ExecutionTarget(t *testing.T) { require.NoError(t, err) defer close() } - - got, err := instance.Client.ActionV3Alpha.GetTarget(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return + retryDuration := 5 * time.Second + if ctxDeadline, ok := isolatedIAMOwnerCTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) } - require.NoError(t, err) - integration.AssertResourceDetails(t, tt.want.GetTarget().GetDetails(), got.GetTarget().GetDetails()) - assert.Equal(t, tt.want.GetTarget().GetConfig(), got.GetTarget().GetConfig()) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV3Alpha.GetTarget(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(ttt, err, "Error: "+err.Error()) + } else { + assert.NoError(ttt, err) + } + if err != nil { + return + } + + integration.AssertResourceDetails(t, tt.want.GetTarget().GetDetails(), got.GetTarget().GetDetails()) + assert.Equal(t, tt.want.GetTarget().GetConfig(), got.GetTarget().GetConfig()) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result") + if tt.clean != nil { tt.clean(tt.ctx) } diff --git a/internal/api/grpc/resources/object/v3alpha/converter.go b/internal/api/grpc/resources/object/v3alpha/converter.go index c2dc7bcc6d..869311b921 100644 --- a/internal/api/grpc/resources/object/v3alpha/converter.go +++ b/internal/api/grpc/resources/object/v3alpha/converter.go @@ -75,3 +75,18 @@ func SearchQueryPbToQuery(defaults systemdefaults.SystemDefaults, query *resourc } return offset, limit, asc, nil } + +func ResourceOwnerFromOrganization(organization *object.Organization) string { + if organization == nil { + return "" + } + + if organization.GetOrgId() != "" { + return organization.GetOrgId() + } + if organization.GetOrgDomain() != "" { + // TODO get org from domain + return "" + } + return "" +} diff --git a/internal/api/grpc/resources/user/v3alpha/integration_test/server_test.go b/internal/api/grpc/resources/user/v3alpha/integration_test/server_test.go index fa82005eb9..b2b11fd510 100644 --- a/internal/api/grpc/resources/user/v3alpha/integration_test/server_test.go +++ b/internal/api/grpc/resources/user/v3alpha/integration_test/server_test.go @@ -14,50 +14,41 @@ 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 ( - IAMOwnerCTX, SystemCTX context.Context - UserCTX context.Context - Instance *integration.Instance - Client user.ZITADELUsersClient + CTX context.Context ) func TestMain(m *testing.M) { os.Exit(func() int { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) defer cancel() - - Instance = integration.NewInstance(ctx) - - IAMOwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) - SystemCTX = integration.WithSystemAuthorization(ctx) - UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeLogin) - Client = Instance.Client.UserV3Alpha + CTX = ctx return m.Run() }()) } -func ensureFeatureEnabled(t *testing.T, iamOwnerCTX context.Context) { - f, err := Instance.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ +func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ Inheritance: true, }) require.NoError(t, err) if f.UserSchema.GetEnabled() { return } - _, err = Instance.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{ + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ UserSchema: gu.Ptr(true), }) require.NoError(t, err) retryDuration := time.Minute - if ctxDeadline, ok := iamOwnerCTX.Deadline(); ok { + if ctxDeadline, ok := ctx.Deadline(); ok { retryDuration = time.Until(ctxDeadline) } require.EventuallyWithT(t, func(ttt *assert.CollectT) { - f, err := Instance.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ Inheritance: true, }) require.NoError(ttt, err) diff --git a/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go b/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go index 7f15a2c562..c8db0f7f6a 100644 --- a/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go +++ b/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/require" "github.com/zitadel/logging" "google.golang.org/protobuf/types/known/structpb" @@ -19,7 +20,11 @@ import ( ) func TestServer_CreateUser(t *testing.T) { - ensureFeatureEnabled(t, IAMOwnerCTX) + t.Parallel() + instance := integration.NewInstance(CTX) + ensureFeatureEnabled(t, instance) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + schema := []byte(`{ "$schema": "urn:zitadel:schema:v1", "type": "object", @@ -29,7 +34,7 @@ func TestServer_CreateUser(t *testing.T) { } } }`) - schemaResp := Instance.CreateUserSchema(IAMOwnerCTX, schema) + schemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema) permissionSchema := []byte(`{ "$schema": "urn:zitadel:schema:v1", "type": "object", @@ -43,8 +48,8 @@ func TestServer_CreateUser(t *testing.T) { } } }`) - permissionSchemaResp := Instance.CreateUserSchema(IAMOwnerCTX, permissionSchema) - orgResp := Instance.CreateOrganization(IAMOwnerCTX, gofakeit.Name(), gofakeit.Email()) + permissionSchemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, permissionSchema) + orgResp := instance.CreateOrganization(isolatedIAMOwnerCTX, gofakeit.Name(), gofakeit.Email()) type res struct { want *resource_object.Details @@ -60,7 +65,7 @@ func TestServer_CreateUser(t *testing.T) { }{ { name: "user create, no schemaID", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &user.CreateUserRequest{ Organization: &object.Organization{ Property: &object.Organization_OrgId{ @@ -89,7 +94,7 @@ func TestServer_CreateUser(t *testing.T) { }, { name: "user create, no permission", - ctx: UserCTX, + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), req: &user.CreateUserRequest{ Organization: &object.Organization{ Property: &object.Organization_OrgId{ @@ -105,7 +110,7 @@ func TestServer_CreateUser(t *testing.T) { }, { name: "user create, invalid schema permission, owner", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &user.CreateUserRequest{ Organization: &object.Organization{ Property: &object.Organization_OrgId{ @@ -121,7 +126,7 @@ func TestServer_CreateUser(t *testing.T) { }, { name: "user create, no user data", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &user.CreateUserRequest{ Organization: &object.Organization{ Property: &object.Organization_OrgId{ @@ -144,7 +149,7 @@ func TestServer_CreateUser(t *testing.T) { }, { name: "user create, ok", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &user.CreateUserRequest{ Organization: &object.Organization{ Property: &object.Organization_OrgId{ @@ -165,9 +170,10 @@ func TestServer_CreateUser(t *testing.T) { }, }, }, - }, { + }, + { name: "user create, full contact, ok", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &user.CreateUserRequest{ Organization: &object.Organization{ Property: &object.Organization_OrgId{ @@ -204,7 +210,423 @@ func TestServer_CreateUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Instance.Client.UserV3Alpha.CreateUser(tt.ctx, tt.req) + got, err := instance.Client.UserV3Alpha.CreateUser(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.res.want, got.Details) + if tt.res.returnCodeEmail { + require.NotNil(t, got.EmailCode) + } + if tt.res.returnCodePhone { + require.NotNil(t, got.PhoneCode) + } + }) + } +} + +func TestServer_PatchUser(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) + permissionSchema := []byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "urn:zitadel:schema:permission": { + "owner": "r", + "self": "r" + }, + "type": "string" + } + } + }`) + permissionSchemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, permissionSchema) + orgResp := instance.CreateOrganization(isolatedIAMOwnerCTX, gofakeit.Name(), gofakeit.Email()) + + type res struct { + want *resource_object.Details + returnCodeEmail bool + returnCodePhone bool + } + tests := []struct { + name string + ctx context.Context + dep func(req *user.PatchUserRequest) error + req *user.PatchUserRequest + res res + wantErr bool + }{ + { + name: "user patch, no context", + ctx: context.Background(), + dep: func(req *user.PatchUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.PatchUser{ + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + wantErr: true, + }, + { + name: "user patch, no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.PatchUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.PatchUser{ + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + wantErr: true, + }, + { + name: "user patch, invalid schema permission, owner", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.PatchUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.PatchUser{ + SchemaId: gu.Ptr(permissionSchemaResp.GetDetails().GetId()), + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + wantErr: true, + }, + { + name: "user patch, not found", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.PatchUserRequest) error { + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "notexisting", + User: &user.PatchUser{ + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + wantErr: true, + }, + { + name: "user patch, not found, org", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.PatchUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "not existing", + }, + }, + User: &user.PatchUser{ + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + wantErr: true, + }, + { + name: "user patch, no change", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.PatchUserRequest) error { + data := "{\"name\": \"user\"}" + schemaID := schemaResp.GetDetails().GetId() + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaID, []byte(data)) + req.Id = userResp.GetDetails().GetId() + req.User.Data = unmarshalJSON(data) + req.User.SchemaId = gu.Ptr(schemaID) + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.PatchUser{}, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "user patch, schema, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.PatchUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + changedSchemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema) + req.User.SchemaId = gu.Ptr(changedSchemaResp.Details.Id) + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.PatchUser{}, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "user patch, schema and data, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.PatchUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + changedSchemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema) + req.User.SchemaId = gu.Ptr(changedSchemaResp.Details.Id) + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.PatchUser{ + Data: unmarshalJSON("{\"name\": \"changed\"}"), + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "user patch, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.PatchUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.PatchUser{ + Data: unmarshalJSON("{\"name\": \"changed\"}"), + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "user patch, no org, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.PatchUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.PatchUserRequest{ + User: &user.PatchUser{ + Data: unmarshalJSON("{\"name\": \"changed\"}"), + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "user patch, contact email, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.PatchUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.PatchUser{ + Contact: &user.SetContact{ + Email: &user.SetEmail{ + Address: gofakeit.Email(), + Verification: &user.SetEmail_ReturnCode{ReturnCode: &user.ReturnEmailVerificationCode{}}, + }, + }, + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + returnCodeEmail: true, + }, + }, + { + name: "user patch, contact phone, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.PatchUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.PatchUser{ + Contact: &user.SetContact{ + Phone: &user.SetPhone{ + Number: gofakeit.Phone(), + Verification: &user.SetPhone_ReturnCode{ReturnCode: &user.ReturnPhoneVerificationCode{}}, + }, + }, + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + returnCodePhone: true, + }, + }, + { + name: "user patch, full contact, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.PatchUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.PatchUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.PatchUser{ + Data: unmarshalJSON("{\"name\": \"changed\"}"), + Contact: &user.SetContact{ + Email: &user.SetEmail{ + Address: gofakeit.Email(), + Verification: &user.SetEmail_ReturnCode{ReturnCode: &user.ReturnEmailVerificationCode{}}, + }, + Phone: &user.SetPhone{ + Number: gofakeit.Phone(), + Verification: &user.SetPhone_ReturnCode{ReturnCode: &user.ReturnPhoneVerificationCode{}}, + }, + }, + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + returnCodePhone: true, + returnCodeEmail: 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.PatchUser(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return @@ -222,7 +644,11 @@ func TestServer_CreateUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { - ensureFeatureEnabled(t, IAMOwnerCTX) + t.Parallel() + instance := integration.NewInstance(CTX) + ensureFeatureEnabled(t, instance) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + schema := []byte(`{ "$schema": "urn:zitadel:schema:v1", "type": "object", @@ -232,49 +658,66 @@ func TestServer_DeleteUser(t *testing.T) { } } }`) - schemaResp := Instance.CreateUserSchema(IAMOwnerCTX, schema) - orgResp := Instance.CreateOrganization(IAMOwnerCTX, gofakeit.Name(), gofakeit.Email()) + schemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema) + orgResp := instance.CreateOrganization(isolatedIAMOwnerCTX, gofakeit.Name(), gofakeit.Email()) tests := []struct { name string ctx context.Context - dep func(ctx context.Context, req *user.DeleteUserRequest) error + dep func(req *user.DeleteUserRequest) error req *user.DeleteUserRequest want *resource_object.Details wantErr bool }{ { name: "user delete, no userID", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &user.DeleteUserRequest{ Organization: &object.Organization{ Property: &object.Organization_OrgId{ OrgId: orgResp.GetOrganizationId(), }, }, - UserId: "", + Id: "", }, wantErr: true, }, { name: "user delete, not existing", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &user.DeleteUserRequest{ Organization: &object.Organization{ Property: &object.Organization_OrgId{ OrgId: orgResp.GetOrganizationId(), }, }, - UserId: "notexisting", + Id: "notexisting", + }, + wantErr: true, + }, + { + name: "user delete, not existing, org", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.DeleteUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, }, wantErr: true, }, { name: "user delete, no context", ctx: context.Background(), - dep: func(ctx context.Context, req *user.DeleteUserRequest) error { - userResp := Instance.CreateSchemaUser(IAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) - req.UserId = userResp.GetDetails().GetId() + dep: func(req *user.DeleteUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() return nil }, req: &user.DeleteUserRequest{ @@ -288,10 +731,10 @@ func TestServer_DeleteUser(t *testing.T) { }, { name: "user delete, no permission", - ctx: UserCTX, - dep: func(ctx context.Context, req *user.DeleteUserRequest) error { - userResp := Instance.CreateSchemaUser(IAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) - req.UserId = userResp.GetDetails().GetId() + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.DeleteUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() return nil }, req: &user.DeleteUserRequest{ @@ -305,10 +748,75 @@ func TestServer_DeleteUser(t *testing.T) { }, { name: "user delete, ok", - ctx: IAMOwnerCTX, - dep: func(ctx context.Context, req *user.DeleteUserRequest) error { - userResp := Instance.CreateSchemaUser(ctx, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) - req.UserId = userResp.GetDetails().GetId() + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.DeleteUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + { + name: "user delete, no org, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.DeleteUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeleteUserRequest{}, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + { + name: "user delete, locked, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.DeleteUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.LockSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + { + name: "user delete, deactivated, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.DeleteUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.DeactivateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) return nil }, req: &user.DeleteUserRequest{ @@ -330,10 +838,10 @@ func TestServer_DeleteUser(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.dep != nil { - err := tt.dep(tt.ctx, tt.req) + err := tt.dep(tt.req) require.NoError(t, err) } - got, err := Instance.Client.UserV3Alpha.DeleteUser(tt.ctx, tt.req) + got, err := instance.Client.UserV3Alpha.DeleteUser(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return @@ -352,3 +860,773 @@ func unmarshalJSON(data string) *structpb.Struct { } return user } + +func TestServer_LockUser(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()) + + tests := []struct { + name string + ctx context.Context + dep func(req *user.LockUserRequest) error + req *user.LockUserRequest + want *resource_object.Details + wantErr bool + }{ + { + name: "user lock, no userID", + ctx: isolatedIAMOwnerCTX, + req: &user.LockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "", + }, + wantErr: true, + }, + { + name: "user lock, not existing", + ctx: isolatedIAMOwnerCTX, + req: &user.LockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "notexisting", + }, + wantErr: true, + }, + { + name: "user lock, not existing in org", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.LockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.LockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, + }, + wantErr: true, + }, + { + name: "user lock, no context", + ctx: context.Background(), + dep: func(req *user.LockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.LockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user lock, no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.LockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.LockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user lock, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.LockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.LockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + { + name: "user lock, no org, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.LockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.LockUserRequest{}, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + { + name: "user lock, already locked", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.LockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.LockSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.LockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user lock, deactivated", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.LockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.DeactivateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.LockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + 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.LockUser(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want, got.Details) + }) + } +} + +func TestServer_UnlockUser(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()) + + tests := []struct { + name string + ctx context.Context + dep func(req *user.UnlockUserRequest) error + req *user.UnlockUserRequest + want *resource_object.Details + wantErr bool + }{ + { + name: "user unlock, no userID", + ctx: isolatedIAMOwnerCTX, + req: &user.UnlockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "", + }, + wantErr: true, + }, + { + name: "user unlock, not existing", + ctx: isolatedIAMOwnerCTX, + req: &user.UnlockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "notexisting", + }, + wantErr: true, + }, + { + name: "user unlock, not existing in org", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.UnlockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.UnlockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, + }, + wantErr: true, + }, + { + name: "user unlock, no context", + ctx: context.Background(), + dep: func(req *user.UnlockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.LockSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.UnlockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user unlock, no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.UnlockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.LockSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.UnlockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user unlock, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.UnlockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.LockSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.UnlockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + { + name: "user unlock,no org, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.UnlockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.LockSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.UnlockUserRequest{}, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + { + name: "user unlock, already unlocked", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.UnlockUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.LockSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + instance.UnlockSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.UnlockUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: 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.UnlockUser(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want, got.Details) + }) + } +} + +func TestServer_DeactivateUser(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()) + + tests := []struct { + name string + ctx context.Context + dep func(req *user.DeactivateUserRequest) error + req *user.DeactivateUserRequest + want *resource_object.Details + wantErr bool + }{ + { + name: "user deactivate, no userID", + ctx: isolatedIAMOwnerCTX, + req: &user.DeactivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "", + }, + wantErr: true, + }, + { + name: "user deactivate, not existing", + ctx: isolatedIAMOwnerCTX, + req: &user.DeactivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "notexisting", + }, + wantErr: true, + }, + { + name: "user deactivate, not existing in org", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.DeactivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeactivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, + }, + wantErr: true, + }, + { + name: "user deactivate, no context", + ctx: context.Background(), + dep: func(req *user.DeactivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeactivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user deactivate, no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.DeactivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeactivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user deactivate, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.DeactivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeactivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + { + name: "user deactivate, no org, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.DeactivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeactivateUserRequest{}, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + { + name: "user deactivate, already deactivated", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.DeactivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.DeactivateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.DeactivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user deactivate, locked", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.DeactivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.LockSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.DeactivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + 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.DeactivateUser(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want, got.Details) + }) + } +} + +func TestServer_ActivateUser(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()) + + tests := []struct { + name string + ctx context.Context + dep func(req *user.ActivateUserRequest) error + req *user.ActivateUserRequest + want *resource_object.Details + wantErr bool + }{ + { + name: "user activate, no userID", + ctx: isolatedIAMOwnerCTX, + req: &user.ActivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "", + }, + wantErr: true, + }, + { + name: "user activate, not existing", + ctx: isolatedIAMOwnerCTX, + req: &user.ActivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "notexisting", + }, + wantErr: true, + }, + { + name: "user activate, not existing in org", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.ActivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.ActivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, + }, + wantErr: true, + }, + { + name: "user activate, no context", + ctx: context.Background(), + dep: func(req *user.ActivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.DeactivateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.ActivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user activate, no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.ActivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.DeactivateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.ActivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user activate, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.ActivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.DeactivateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.ActivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + { + name: "user activate, no org, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.ActivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.DeactivateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.ActivateUserRequest{}, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + { + name: "user activate, already activated", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.ActivateUserRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.DeactivateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + instance.ActivateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.ActivateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: 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.ActivateUser(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want, got.Details) + }) + } +} diff --git a/internal/api/grpc/resources/user/v3alpha/user.go b/internal/api/grpc/resources/user/v3alpha/user.go index ede2f122aa..7c3f2a750e 100644 --- a/internal/api/grpc/resources/user/v3alpha/user.go +++ b/internal/api/grpc/resources/user/v3alpha/user.go @@ -8,6 +8,7 @@ import ( "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" @@ -26,7 +27,7 @@ func (s *Server) CreateUser(ctx context.Context, req *user.CreateUserRequest) (_ return nil, err } return &user.CreateUserResponse{ - Details: resource_object.DomainToDetailsPb(schemauser.Details, object.OwnerType_OWNER_TYPE_ORG, schemauser.ResourceOwner), + Details: resource_object.DomainToDetailsPb(schemauser.Details, object.OwnerType_OWNER_TYPE_ORG, schemauser.Details.ResourceOwner), EmailCode: gu.Ptr(schemauser.ReturnCodeEmail), PhoneCode: gu.Ptr(schemauser.ReturnCodePhone), }, nil @@ -37,19 +38,35 @@ func createUserRequestToCreateSchemaUser(ctx context.Context, req *user.CreateUs if err != nil { return nil, err } + return &command.CreateSchemaUser{ - ResourceOwner: authz.GetCtxData(ctx).OrgID, + ResourceOwner: organizationToCreateResourceOwner(ctx, req.Organization), SchemaID: req.GetUser().GetSchemaId(), ID: req.GetUser().GetUserId(), Data: data, }, nil } +func organizationToCreateResourceOwner(ctx context.Context, org *object.Organization) string { + resourceOwner := authz.GetCtxData(ctx).OrgID + if resourceOwnerReq := resource_object.ResourceOwnerFromOrganization(org); resourceOwnerReq != "" { + return resourceOwnerReq + } + return resourceOwner +} + +func organizationToUpdateResourceOwner(org *object.Organization) string { + if resourceOwnerReq := resource_object.ResourceOwnerFromOrganization(org); resourceOwnerReq != "" { + return resourceOwnerReq + } + return "" +} + func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) { if err := checkUserSchemaEnabled(ctx); err != nil { return nil, err } - details, err := s.command.DeleteSchemaUser(ctx, req.GetUserId()) + details, err := s.command.DeleteSchemaUser(ctx, organizationToUpdateResourceOwner(req.Organization), req.GetId()) if err != nil { return nil, err } @@ -64,3 +81,126 @@ func checkUserSchemaEnabled(ctx context.Context) error { } return zerrors.ThrowPreconditionFailed(nil, "TODO", "Errors.UserSchema.NotEnabled") } + +func (s *Server) PatchUser(ctx context.Context, req *user.PatchUserRequest) (_ *user.PatchUserResponse, err error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + schemauser, err := patchUserRequestToChangeSchemaUser(req) + if err != nil { + return nil, err + } + + if err := s.command.ChangeSchemaUser(ctx, schemauser, s.userCodeAlg); 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), + }, nil +} + +func patchUserRequestToChangeSchemaUser(req *user.PatchUserRequest) (_ *command.ChangeSchemaUser, err error) { + var data []byte + if req.GetUser().Data != nil { + data, err = req.GetUser().GetData().MarshalJSON() + if err != nil { + return nil, err + } + } + + 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, + }, nil +} + +func (s *Server) DeactivateUser(ctx context.Context, req *user.DeactivateUserRequest) (_ *user.DeactivateUserResponse, err error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + + details, err := s.command.DeactivateSchemaUser(ctx, organizationToUpdateResourceOwner(req.Organization), req.GetId()) + if err != nil { + return nil, err + } + return &user.DeactivateUserResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), + }, nil +} + +func (s *Server) ActivateUser(ctx context.Context, req *user.ActivateUserRequest) (_ *user.ActivateUserResponse, err error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + + details, err := s.command.ActivateSchemaUser(ctx, organizationToUpdateResourceOwner(req.Organization), req.GetId()) + if err != nil { + return nil, err + } + return &user.ActivateUserResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), + }, nil +} + +func (s *Server) LockUser(ctx context.Context, req *user.LockUserRequest) (_ *user.LockUserResponse, err error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + + details, err := s.command.LockSchemaUser(ctx, organizationToUpdateResourceOwner(req.Organization), req.GetId()) + if err != nil { + return nil, err + } + return &user.LockUserResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), + }, nil +} + +func (s *Server) UnlockUser(ctx context.Context, req *user.UnlockUserRequest) (_ *user.UnlockUserResponse, err error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + + details, err := s.command.UnlockSchemaUser(ctx, organizationToUpdateResourceOwner(req.Organization), req.GetId()) + if err != nil { + return nil, err + } + return &user.UnlockUserResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), + }, nil +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/integration_test/query_test.go b/internal/api/grpc/resources/userschema/v3alpha/integration_test/query_test.go index e7aaf081ed..36cf032660 100644 --- a/internal/api/grpc/resources/userschema/v3alpha/integration_test/query_test.go +++ b/internal/api/grpc/resources/userschema/v3alpha/integration_test/query_test.go @@ -20,7 +20,10 @@ import ( ) func TestServer_ListUserSchemas(t *testing.T) { - ensureFeatureEnabled(t, IAMOwnerCTX) + t.Parallel() + instance := integration.NewInstance(CTX) + ensureFeatureEnabled(t, instance) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) userSchema := new(structpb.Struct) err := userSchema.UnmarshalJSON([]byte(`{ @@ -43,7 +46,7 @@ func TestServer_ListUserSchemas(t *testing.T) { { name: "missing permission", args: args{ - ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), req: &schema.SearchUserSchemasRequest{}, }, wantErr: true, @@ -51,7 +54,7 @@ func TestServer_ListUserSchemas(t *testing.T) { { name: "not found, error", args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.SearchUserSchemasRequest{ Filters: []*schema.SearchFilter{ { @@ -75,11 +78,11 @@ func TestServer_ListUserSchemas(t *testing.T) { { name: "single (id), ok", args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.SearchUserSchemasRequest{}, prepare: func(request *schema.SearchUserSchemasRequest, resp *schema.SearchUserSchemasResponse) error { schemaType := gofakeit.Name() - createResp := Instance.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType) + createResp := instance.CreateUserSchemaEmptyWithType(isolatedIAMOwnerCTX, schemaType) request.Filters = []*schema.SearchFilter{ { Filter: &schema.SearchFilter_IdFilter{ @@ -121,14 +124,14 @@ func TestServer_ListUserSchemas(t *testing.T) { { name: "multiple (type), ok", args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.SearchUserSchemasRequest{}, prepare: func(request *schema.SearchUserSchemasRequest, resp *schema.SearchUserSchemasResponse) error { schemaType := gofakeit.Name() schemaType1 := schemaType + "_1" schemaType2 := schemaType + "_2" - createResp := Instance.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType1) - createResp2 := Instance.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType2) + createResp := instance.CreateUserSchemaEmptyWithType(isolatedIAMOwnerCTX, schemaType1) + createResp2 := instance.CreateUserSchemaEmptyWithType(isolatedIAMOwnerCTX, schemaType2) request.SortingColumn = gu.Ptr(schema.FieldName_FIELD_NAME_TYPE) request.Query = &object.SearchQuery{Desc: false} @@ -186,12 +189,12 @@ func TestServer_ListUserSchemas(t *testing.T) { } retryDuration := 20 * time.Second - if ctxDeadline, ok := IAMOwnerCTX.Deadline(); ok { + if ctxDeadline, ok := isolatedIAMOwnerCTX.Deadline(); ok { retryDuration = time.Until(ctxDeadline) } require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.SearchUserSchemas(tt.args.ctx, tt.args.req) + got, err := instance.Client.UserSchemaV3.SearchUserSchemas(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(ttt, err) return @@ -215,7 +218,10 @@ func TestServer_ListUserSchemas(t *testing.T) { } func TestServer_GetUserSchema(t *testing.T) { - ensureFeatureEnabled(t, IAMOwnerCTX) + t.Parallel() + instance := integration.NewInstance(CTX) + ensureFeatureEnabled(t, instance) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) userSchema := new(structpb.Struct) err := userSchema.UnmarshalJSON([]byte(`{ @@ -238,11 +244,11 @@ func TestServer_GetUserSchema(t *testing.T) { { name: "missing permission", args: args{ - ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), req: &schema.GetUserSchemaRequest{}, prepare: func(request *schema.GetUserSchemaRequest, resp *schema.GetUserSchemaResponse) error { schemaType := gofakeit.Name() - createResp := Instance.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType) + createResp := instance.CreateUserSchemaEmptyWithType(isolatedIAMOwnerCTX, schemaType) request.Id = createResp.GetDetails().GetId() return nil }, @@ -252,7 +258,7 @@ func TestServer_GetUserSchema(t *testing.T) { { name: "not existing, error", args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.GetUserSchemaRequest{ Id: "notexisting", }, @@ -262,11 +268,11 @@ func TestServer_GetUserSchema(t *testing.T) { { name: "get, ok", args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.GetUserSchemaRequest{}, prepare: func(request *schema.GetUserSchemaRequest, resp *schema.GetUserSchemaResponse) error { schemaType := gofakeit.Name() - createResp := Instance.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType) + createResp := instance.CreateUserSchemaEmptyWithType(isolatedIAMOwnerCTX, schemaType) request.Id = createResp.GetDetails().GetId() resp.UserSchema.Config.Type = schemaType @@ -295,12 +301,12 @@ func TestServer_GetUserSchema(t *testing.T) { } retryDuration := 5 * time.Second - if ctxDeadline, ok := IAMOwnerCTX.Deadline(); ok { + if ctxDeadline, ok := isolatedIAMOwnerCTX.Deadline(); ok { retryDuration = time.Until(ctxDeadline) } require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.GetUserSchema(tt.args.ctx, tt.args.req) + got, err := instance.Client.UserSchemaV3.GetUserSchema(tt.args.ctx, tt.args.req) if tt.wantErr { assert.Error(t, err, "Error: "+err.Error()) } else { diff --git a/internal/api/grpc/resources/userschema/v3alpha/integration_test/server_test.go b/internal/api/grpc/resources/userschema/v3alpha/integration_test/server_test.go index fbdef9bee8..f779a98d87 100644 --- a/internal/api/grpc/resources/userschema/v3alpha/integration_test/server_test.go +++ b/internal/api/grpc/resources/userschema/v3alpha/integration_test/server_test.go @@ -14,49 +14,41 @@ 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 ( - IAMOwnerCTX, SystemCTX context.Context - Instance *integration.Instance - Client schema.ZITADELUserSchemasClient + CTX context.Context ) func TestMain(m *testing.M) { os.Exit(func() int { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) defer cancel() - - Instance = integration.NewInstance(ctx) - - IAMOwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) - SystemCTX = integration.WithSystemAuthorization(ctx) - Client = Instance.Client.UserSchemaV3 - + CTX = ctx return m.Run() }()) } -func ensureFeatureEnabled(t *testing.T, iamOwnerCTX context.Context) { - f, err := Instance.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ +func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ Inheritance: true, }) require.NoError(t, err) if f.UserSchema.GetEnabled() { return } - _, err = Instance.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{ + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ UserSchema: gu.Ptr(true), }) require.NoError(t, err) retryDuration := time.Minute - if ctxDeadline, ok := iamOwnerCTX.Deadline(); ok { + if ctxDeadline, ok := ctx.Deadline(); ok { retryDuration = time.Until(ctxDeadline) } require.EventuallyWithT(t, func(ttt *assert.CollectT) { - f, err := Instance.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ Inheritance: true, }) require.NoError(ttt, err) diff --git a/internal/api/grpc/resources/userschema/v3alpha/integration_test/userschema_test.go b/internal/api/grpc/resources/userschema/v3alpha/integration_test/userschema_test.go index f7216f8426..a264b163eb 100644 --- a/internal/api/grpc/resources/userschema/v3alpha/integration_test/userschema_test.go +++ b/internal/api/grpc/resources/userschema/v3alpha/integration_test/userschema_test.go @@ -19,7 +19,10 @@ import ( ) func TestServer_CreateUserSchema(t *testing.T) { - ensureFeatureEnabled(t, IAMOwnerCTX) + t.Parallel() + instance := integration.NewInstance(CTX) + ensureFeatureEnabled(t, instance) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) tests := []struct { name string @@ -30,7 +33,7 @@ func TestServer_CreateUserSchema(t *testing.T) { }{ { name: "missing permission, error", - ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), req: &schema.CreateUserSchemaRequest{ UserSchema: &schema.UserSchema{ Type: gofakeit.Name(), @@ -40,7 +43,7 @@ func TestServer_CreateUserSchema(t *testing.T) { }, { name: "empty type", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.CreateUserSchemaRequest{ UserSchema: &schema.UserSchema{ Type: "", @@ -50,7 +53,7 @@ func TestServer_CreateUserSchema(t *testing.T) { }, { name: "empty schema, error", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.CreateUserSchemaRequest{ UserSchema: &schema.UserSchema{ Type: gofakeit.Name(), @@ -60,7 +63,7 @@ func TestServer_CreateUserSchema(t *testing.T) { }, { name: "invalid schema, error", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.CreateUserSchemaRequest{ UserSchema: &schema.UserSchema{ Type: gofakeit.Name(), @@ -91,7 +94,7 @@ func TestServer_CreateUserSchema(t *testing.T) { }, { name: "no authenticators, ok", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.CreateUserSchemaRequest{ UserSchema: &schema.UserSchema{ Type: gofakeit.Name(), @@ -123,14 +126,14 @@ func TestServer_CreateUserSchema(t *testing.T) { Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Instance.ID(), + Id: instance.ID(), }, }, }, }, { name: "invalid authenticator, error", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.CreateUserSchemaRequest{ UserSchema: &schema.UserSchema{ Type: gofakeit.Name(), @@ -164,7 +167,7 @@ func TestServer_CreateUserSchema(t *testing.T) { }, { name: "with authenticator, ok", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.CreateUserSchemaRequest{ UserSchema: &schema.UserSchema{ Type: gofakeit.Name(), @@ -199,14 +202,14 @@ func TestServer_CreateUserSchema(t *testing.T) { Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Instance.ID(), + Id: instance.ID(), }, }, }, }, { name: "with invalid permission, error", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.CreateUserSchemaRequest{ UserSchema: &schema.UserSchema{ Type: gofakeit.Name(), @@ -241,7 +244,7 @@ func TestServer_CreateUserSchema(t *testing.T) { }, { name: "with valid permission, ok", - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.CreateUserSchemaRequest{ UserSchema: &schema.UserSchema{ Type: gofakeit.Name(), @@ -280,7 +283,7 @@ func TestServer_CreateUserSchema(t *testing.T) { Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Instance.ID(), + Id: instance.ID(), }, }, }, @@ -288,7 +291,7 @@ func TestServer_CreateUserSchema(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.CreateUserSchema(tt.ctx, tt.req) + got, err := instance.Client.UserSchemaV3.CreateUserSchema(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return @@ -301,7 +304,10 @@ func TestServer_CreateUserSchema(t *testing.T) { } func TestServer_UpdateUserSchema(t *testing.T) { - ensureFeatureEnabled(t, IAMOwnerCTX) + t.Parallel() + instance := integration.NewInstance(CTX) + ensureFeatureEnabled(t, instance) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) type args struct { ctx context.Context @@ -317,12 +323,12 @@ func TestServer_UpdateUserSchema(t *testing.T) { { name: "missing permission, error", prepare: func(request *schema.PatchUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID return nil }, args: args{ - ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), req: &schema.PatchUserSchemaRequest{ UserSchema: &schema.PatchUserSchema{ Type: gu.Ptr(gofakeit.Name()), @@ -337,7 +343,7 @@ func TestServer_UpdateUserSchema(t *testing.T) { return nil }, args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.PatchUserSchemaRequest{}, }, wantErr: true, @@ -349,7 +355,7 @@ func TestServer_UpdateUserSchema(t *testing.T) { return nil }, args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.PatchUserSchemaRequest{}, }, wantErr: true, @@ -357,12 +363,12 @@ func TestServer_UpdateUserSchema(t *testing.T) { { name: "empty type, error", prepare: func(request *schema.PatchUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID return nil }, args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.PatchUserSchemaRequest{ UserSchema: &schema.PatchUserSchema{ Type: gu.Ptr(""), @@ -374,12 +380,12 @@ func TestServer_UpdateUserSchema(t *testing.T) { { name: "update type, ok", prepare: func(request *schema.PatchUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID return nil }, args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.PatchUserSchemaRequest{ UserSchema: &schema.PatchUserSchema{ Type: gu.Ptr(gofakeit.Name()), @@ -391,7 +397,7 @@ func TestServer_UpdateUserSchema(t *testing.T) { Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Instance.ID(), + Id: instance.ID(), }, }, }, @@ -399,12 +405,12 @@ func TestServer_UpdateUserSchema(t *testing.T) { { name: "empty schema, ok", prepare: func(request *schema.PatchUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID return nil }, args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.PatchUserSchemaRequest{ UserSchema: &schema.PatchUserSchema{ DataType: &schema.PatchUserSchema_Schema{}, @@ -416,7 +422,7 @@ func TestServer_UpdateUserSchema(t *testing.T) { Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Instance.ID(), + Id: instance.ID(), }, }, }, @@ -424,12 +430,12 @@ func TestServer_UpdateUserSchema(t *testing.T) { { name: "invalid schema, error", prepare: func(request *schema.PatchUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID return nil }, args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.PatchUserSchemaRequest{ UserSchema: &schema.PatchUserSchema{ DataType: &schema.PatchUserSchema_Schema{ @@ -462,12 +468,12 @@ func TestServer_UpdateUserSchema(t *testing.T) { { name: "update schema, ok", prepare: func(request *schema.PatchUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID return nil }, args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.PatchUserSchemaRequest{ UserSchema: &schema.PatchUserSchema{ DataType: &schema.PatchUserSchema_Schema{ @@ -500,7 +506,7 @@ func TestServer_UpdateUserSchema(t *testing.T) { Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Instance.ID(), + Id: instance.ID(), }, }, }, @@ -508,12 +514,12 @@ func TestServer_UpdateUserSchema(t *testing.T) { { name: "invalid authenticator, error", prepare: func(request *schema.PatchUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID return nil }, args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.PatchUserSchemaRequest{ UserSchema: &schema.PatchUserSchema{ PossibleAuthenticators: []schema.AuthenticatorType{ @@ -527,12 +533,12 @@ func TestServer_UpdateUserSchema(t *testing.T) { { name: "update authenticator, ok", prepare: func(request *schema.PatchUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID return nil }, args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.PatchUserSchemaRequest{ UserSchema: &schema.PatchUserSchema{ PossibleAuthenticators: []schema.AuthenticatorType{ @@ -546,7 +552,7 @@ func TestServer_UpdateUserSchema(t *testing.T) { Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Instance.ID(), + Id: instance.ID(), }, }, }, @@ -554,8 +560,8 @@ func TestServer_UpdateUserSchema(t *testing.T) { { name: "inactive, error", prepare: func(request *schema.PatchUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() - _, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() + _, err := instance.Client.UserSchemaV3.DeactivateUserSchema(isolatedIAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ Id: schemaID, }) require.NoError(t, err) @@ -563,7 +569,7 @@ func TestServer_UpdateUserSchema(t *testing.T) { return nil }, args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.PatchUserSchemaRequest{ UserSchema: &schema.PatchUserSchema{ Type: gu.Ptr(gofakeit.Name()), @@ -578,7 +584,7 @@ func TestServer_UpdateUserSchema(t *testing.T) { err := tt.prepare(tt.args.req) require.NoError(t, err) - got, err := Client.PatchUserSchema(tt.args.ctx, tt.args.req) + got, err := instance.Client.UserSchemaV3.PatchUserSchema(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) return @@ -590,7 +596,10 @@ func TestServer_UpdateUserSchema(t *testing.T) { } func TestServer_DeactivateUserSchema(t *testing.T) { - ensureFeatureEnabled(t, IAMOwnerCTX) + t.Parallel() + instance := integration.NewInstance(CTX) + ensureFeatureEnabled(t, instance) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) type args struct { ctx context.Context @@ -606,7 +615,7 @@ func TestServer_DeactivateUserSchema(t *testing.T) { { name: "not existing, error", args: args{ - IAMOwnerCTX, + isolatedIAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ Id: "notexisting", }, @@ -617,10 +626,10 @@ func TestServer_DeactivateUserSchema(t *testing.T) { { name: "active, ok", args: args{ - IAMOwnerCTX, + isolatedIAMOwnerCTX, &schema.DeactivateUserSchemaRequest{}, func(request *schema.DeactivateUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID return nil }, @@ -630,7 +639,7 @@ func TestServer_DeactivateUserSchema(t *testing.T) { Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Instance.ID(), + Id: instance.ID(), }, }, }, @@ -638,12 +647,12 @@ func TestServer_DeactivateUserSchema(t *testing.T) { { name: "inactive, error", args: args{ - IAMOwnerCTX, + isolatedIAMOwnerCTX, &schema.DeactivateUserSchemaRequest{}, func(request *schema.DeactivateUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID - _, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ + _, err := instance.Client.UserSchemaV3.DeactivateUserSchema(isolatedIAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ Id: schemaID, }) return err @@ -657,7 +666,7 @@ func TestServer_DeactivateUserSchema(t *testing.T) { err := tt.args.prepare(tt.args.req) require.NoError(t, err) - got, err := Client.DeactivateUserSchema(tt.args.ctx, tt.args.req) + got, err := instance.Client.UserSchemaV3.DeactivateUserSchema(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) return @@ -669,7 +678,10 @@ func TestServer_DeactivateUserSchema(t *testing.T) { } func TestServer_ReactivateUserSchema(t *testing.T) { - ensureFeatureEnabled(t, IAMOwnerCTX) + t.Parallel() + instance := integration.NewInstance(CTX) + ensureFeatureEnabled(t, instance) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) type args struct { ctx context.Context @@ -685,7 +697,7 @@ func TestServer_ReactivateUserSchema(t *testing.T) { { name: "not existing, error", args: args{ - IAMOwnerCTX, + isolatedIAMOwnerCTX, &schema.ReactivateUserSchemaRequest{ Id: "notexisting", }, @@ -696,10 +708,10 @@ func TestServer_ReactivateUserSchema(t *testing.T) { { name: "active, error", args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.ReactivateUserSchemaRequest{}, prepare: func(request *schema.ReactivateUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID return nil }, @@ -709,12 +721,12 @@ func TestServer_ReactivateUserSchema(t *testing.T) { { name: "inactive, ok", args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.ReactivateUserSchemaRequest{}, prepare: func(request *schema.ReactivateUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID - _, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ + _, err := instance.Client.UserSchemaV3.DeactivateUserSchema(isolatedIAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ Id: schemaID, }) return err @@ -725,7 +737,7 @@ func TestServer_ReactivateUserSchema(t *testing.T) { Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Instance.ID(), + Id: instance.ID(), }, }, }, @@ -736,7 +748,7 @@ func TestServer_ReactivateUserSchema(t *testing.T) { err := tt.args.prepare(tt.args.req) require.NoError(t, err) - got, err := Client.ReactivateUserSchema(tt.args.ctx, tt.args.req) + got, err := instance.Client.UserSchemaV3.ReactivateUserSchema(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) return @@ -748,7 +760,10 @@ func TestServer_ReactivateUserSchema(t *testing.T) { } func TestServer_DeleteUserSchema(t *testing.T) { - ensureFeatureEnabled(t, IAMOwnerCTX) + t.Parallel() + instance := integration.NewInstance(CTX) + ensureFeatureEnabled(t, instance) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) type args struct { ctx context.Context @@ -764,7 +779,7 @@ func TestServer_DeleteUserSchema(t *testing.T) { { name: "not existing, error", args: args{ - IAMOwnerCTX, + isolatedIAMOwnerCTX, &schema.DeleteUserSchemaRequest{ Id: "notexisting", }, @@ -775,10 +790,10 @@ func TestServer_DeleteUserSchema(t *testing.T) { { name: "delete, ok", args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.DeleteUserSchemaRequest{}, prepare: func(request *schema.DeleteUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID return nil }, @@ -788,7 +803,7 @@ func TestServer_DeleteUserSchema(t *testing.T) { Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Instance.ID(), + Id: instance.ID(), }, }, }, @@ -796,12 +811,12 @@ func TestServer_DeleteUserSchema(t *testing.T) { { name: "deleted, error", args: args{ - ctx: IAMOwnerCTX, + ctx: isolatedIAMOwnerCTX, req: &schema.DeleteUserSchemaRequest{}, prepare: func(request *schema.DeleteUserSchemaRequest) error { - schemaID := Instance.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + schemaID := instance.CreateUserSchemaEmpty(isolatedIAMOwnerCTX).GetDetails().GetId() request.Id = schemaID - _, err := Client.DeleteUserSchema(IAMOwnerCTX, &schema.DeleteUserSchemaRequest{ + _, err := instance.Client.UserSchemaV3.DeleteUserSchema(isolatedIAMOwnerCTX, &schema.DeleteUserSchemaRequest{ Id: schemaID, }) return err @@ -815,7 +830,7 @@ func TestServer_DeleteUserSchema(t *testing.T) { err := tt.args.prepare(tt.args.req) require.NoError(t, err) - got, err := Client.DeleteUserSchema(tt.args.ctx, tt.args.req) + got, err := instance.Client.UserSchemaV3.DeleteUserSchema(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) return diff --git a/internal/command/user_schema.go b/internal/command/user_schema.go index 7b53c9e165..3120071d55 100644 --- a/internal/command/user_schema.go +++ b/internal/command/user_schema.go @@ -186,7 +186,7 @@ func validateUserSchema(userSchema json.RawMessage) error { } func (c *Commands) getSchemaWriteModelByID(ctx context.Context, resourceOwner, id string) (*UserSchemaWriteModel, error) { - writeModel := NewUserSchemaWriteModel(resourceOwner, id, "") + writeModel := NewUserSchemaWriteModel(resourceOwner, id) if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { return nil, err } diff --git a/internal/command/user_schema_model.go b/internal/command/user_schema_model.go index e8df80479a..08352e6d57 100644 --- a/internal/command/user_schema_model.go +++ b/internal/command/user_schema_model.go @@ -19,16 +19,15 @@ type UserSchemaWriteModel struct { Schema json.RawMessage PossibleAuthenticators []domain.AuthenticatorType State domain.UserSchemaState - Revision uint64 + SchemaRevision uint64 } -func NewUserSchemaWriteModel(resourceOwner, schemaID, ty string) *UserSchemaWriteModel { +func NewUserSchemaWriteModel(resourceOwner, schemaID string) *UserSchemaWriteModel { return &UserSchemaWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: schemaID, ResourceOwner: resourceOwner, }, - SchemaType: ty, } } @@ -40,13 +39,13 @@ func (wm *UserSchemaWriteModel) Reduce() error { wm.Schema = e.Schema wm.PossibleAuthenticators = e.PossibleAuthenticators wm.State = domain.UserSchemaStateActive - wm.Revision = 1 + wm.SchemaRevision = 1 case *schema.UpdatedEvent: if e.SchemaType != nil { wm.SchemaType = *e.SchemaType } if e.SchemaRevision != nil { - wm.Revision = *e.SchemaRevision + wm.SchemaRevision = *e.SchemaRevision } if len(e.Schema) > 0 { wm.Schema = e.Schema @@ -79,10 +78,6 @@ func (wm *UserSchemaWriteModel) Query() *eventstore.SearchQueryBuilder { schema.DeletedType, ) - if wm.SchemaType != "" { - query = query.EventData(map[string]interface{}{"schemaType": wm.SchemaType}) - } - return query.Builder() } func (wm *UserSchemaWriteModel) NewUpdatedEvent( @@ -99,7 +94,7 @@ func (wm *UserSchemaWriteModel) NewUpdatedEvent( if !bytes.Equal(wm.Schema, userSchema) { changes = append(changes, schema.ChangeSchema(userSchema)) // change revision if the content of the schema changed - changes = append(changes, schema.IncreaseRevision(wm.Revision)) + changes = append(changes, schema.IncreaseRevision(wm.SchemaRevision)) } if len(possibleAuthenticators) > 0 && slices.Compare(wm.PossibleAuthenticators, possibleAuthenticators) != 0 { changes = append(changes, schema.ChangePossibleAuthenticators(possibleAuthenticators)) diff --git a/internal/command/user_v3.go b/internal/command/user_v3.go index f2cacd6cb0..d5a097ce27 100644 --- a/internal/command/user_v3.go +++ b/internal/command/user_v3.go @@ -15,14 +15,14 @@ import ( ) type CreateSchemaUser struct { - Details *domain.ObjectDetails - ResourceOwner string + Details *domain.ObjectDetails SchemaID string schemaRevision uint64 - ID string - Data json.RawMessage + ResourceOwner string + ID string + Data json.RawMessage Email *Email ReturnCodeEmail string @@ -45,7 +45,7 @@ func (s *CreateSchemaUser) Valid(ctx context.Context, c *Commands) (err error) { if !schemaWriteModel.Exists() { return zerrors.ThrowPreconditionFailed(nil, "COMMAND-N9QOuN4F7o", "Errors.UserSchema.NotExists") } - s.schemaRevision = schemaWriteModel.Revision + s.schemaRevision = schemaWriteModel.SchemaRevision if s.ID == "" { s.ID, err = c.idGenerator.Next() @@ -120,13 +120,13 @@ func (c *Commands) CreateSchemaUser(ctx context.Context, user *CreateSchemaUser, ), } if user.Email != nil { - events, user.ReturnCodeEmail, err = c.updateSchemaUserEmail(ctx, events, userAgg, user.Email, alg) + events, user.ReturnCodeEmail, err = c.updateSchemaUserEmail(ctx, writeModel, events, userAgg, user.Email, alg) if err != nil { return err } } if user.Phone != nil { - events, user.ReturnCodePhone, err = c.updateSchemaUserPhone(ctx, events, userAgg, user.Phone, alg) + events, user.ReturnCodePhone, err = c.updateSchemaUserPhone(ctx, writeModel, events, userAgg, user.Phone, alg) if err != nil { return err } @@ -139,11 +139,11 @@ func (c *Commands) CreateSchemaUser(ctx context.Context, user *CreateSchemaUser, return nil } -func (c *Commands) DeleteSchemaUser(ctx context.Context, id string) (*domain.ObjectDetails, error) { +func (c *Commands) DeleteSchemaUser(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { if id == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Vs4wJCME7T", "Errors.IDMissing") } - writeModel, err := c.getSchemaUserExists(ctx, "", id) + writeModel, err := c.getSchemaUserExists(ctx, resourceOwner, id) if err != nil { return nil, err } @@ -161,7 +161,235 @@ func (c *Commands) DeleteSchemaUser(ctx context.Context, id string) (*domain.Obj return writeModelToObjectDetails(&writeModel.WriteModel), nil } -func (c *Commands) updateSchemaUserEmail(ctx context.Context, events []eventstore.Command, agg *eventstore.Aggregate, email *Email, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) { +type ChangeSchemaUser struct { + Details *domain.ObjectDetails + + SchemaID *string + schemaWriteModel *UserSchemaWriteModel + + ResourceOwner string + ID string + Data json.RawMessage + + Email *Email + ReturnCodeEmail string + Phone *Phone + ReturnCodePhone string +} + +func (s *ChangeSchemaUser) Valid(ctx context.Context, c *Commands) (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 + } + } + + if s.Phone != nil && s.Phone.Number != "" { + if s.Phone.Number, err = s.Phone.Number.Normalize(); err != nil { + return err + } + } + + 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 + } + + writeModel, err := c.getSchemaUserWriteModelByID(ctx, user.ResourceOwner, user.ID) + if err != nil { + return err + } + if !writeModel.Exists() { + return 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) + } + } + if user.Email != nil { + events, user.ReturnCodeEmail, err = c.updateSchemaUserEmail(ctx, writeModel, events, userAgg, user.Email, alg) + if err != nil { + return err + } + } + if user.Phone != nil { + events, user.ReturnCodePhone, err = c.updateSchemaUserPhone(ctx, writeModel, events, userAgg, user.Phone, alg) + if err != nil { + return err + } + } + if len(events) == 0 { + user.Details = writeModelToObjectDetails(&writeModel.WriteModel) + return nil + } + if err := c.pushAppendAndReduce(ctx, writeModel, events...); err != nil { + return err + } + user.Details = writeModelToObjectDetails(&writeModel.WriteModel) + return nil +} + +func (c *Commands) checkPermissionUpdateUserState(ctx context.Context, resourceOwner, userID string) error { + return c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID) +} + +func (c *Commands) LockSchemaUser(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Eu8I2VAfjF", "Errors.IDMissing") + } + writeModel, err := c.getSchemaUserExists(ctx, resourceOwner, id) + if err != nil { + return nil, err + } + if !writeModel.Exists() || writeModel.Locked { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-G4LOrnjY7q", "Errors.User.NotFound") + } + if err := c.checkPermissionUpdateUserState(ctx, writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + if err := c.pushAppendAndReduce(ctx, writeModel, + schemauser.NewLockedEvent(ctx, UserV3AggregateFromWriteModel(&writeModel.WriteModel)), + ); err != nil { + return nil, err + } + return writeModelToObjectDetails(&writeModel.WriteModel), nil +} + +func (c *Commands) UnlockSchemaUser(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-krXtYscQZh", "Errors.IDMissing") + } + writeModel, err := c.getSchemaUserExists(ctx, resourceOwner, id) + if err != nil { + return nil, err + } + if !writeModel.Exists() || !writeModel.Locked { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-gpBv46Lh9m", "Errors.User.NotFound") + } + if err := c.checkPermissionUpdateUserState(ctx, writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + if err := c.pushAppendAndReduce(ctx, writeModel, + schemauser.NewUnlockedEvent(ctx, UserV3AggregateFromWriteModel(&writeModel.WriteModel)), + ); err != nil { + return nil, err + } + return writeModelToObjectDetails(&writeModel.WriteModel), nil +} + +func (c *Commands) DeactivateSchemaUser(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-pjJhge86ZV", "Errors.IDMissing") + } + writeModel, err := c.getSchemaUserExists(ctx, resourceOwner, id) + if err != nil { + return nil, err + } + if writeModel.State != domain.UserStateActive { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-Ob6lR5iFTe", "Errors.User.NotFound") + } + if err := c.checkPermissionUpdateUserState(ctx, writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + if err := c.pushAppendAndReduce(ctx, writeModel, + schemauser.NewDeactivatedEvent(ctx, UserV3AggregateFromWriteModel(&writeModel.WriteModel)), + ); err != nil { + return nil, err + } + return writeModelToObjectDetails(&writeModel.WriteModel), nil +} + +func (c *Commands) ActivateSchemaUser(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-17XupGvxBJ", "Errors.IDMissing") + } + writeModel, err := c.getSchemaUserExists(ctx, "", id) + if err != nil { + return nil, err + } + if writeModel.State != domain.UserStateInactive { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-rQjbBr4J3j", "Errors.User.NotFound") + } + if err := c.checkPermissionUpdateUserState(ctx, writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + if err := c.pushAppendAndReduce(ctx, writeModel, + schemauser.NewActivatedEvent(ctx, UserV3AggregateFromWriteModel(&writeModel.WriteModel)), + ); err != nil { + return nil, err + } + 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, @@ -187,8 +415,12 @@ func (c *Commands) updateSchemaUserEmail(ctx context.Context, events []eventstor return events, plainCode, nil } -func (c *Commands) updateSchemaUserPhone(ctx context.Context, events []eventstore.Command, agg *eventstore.Aggregate, phone *Phone, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) { - events = append(events, schemauser.NewPhoneChangedEvent(ctx, +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, )) @@ -218,3 +450,11 @@ func (c *Commands) getSchemaUserExists(ctx context.Context, resourceOwner, id st } return writeModel, nil } + +func (c *Commands) getSchemaUserWriteModelByID(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) { + writeModel := NewUserV3WriteModel(resourceOwner, id) + if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { + return nil, err + } + return writeModel, nil +} diff --git a/internal/command/user_v3_model.go b/internal/command/user_v3_model.go index 51f783aaed..6231558178 100644 --- a/internal/command/user_v3_model.go +++ b/internal/command/user_v3_model.go @@ -29,7 +29,8 @@ type UserV3WriteModel struct { Data json.RawMessage - State domain.UserState + Locked bool + State domain.UserState } func NewExistsUserV3WriteModel(resourceOwner, userID string) *UserV3WriteModel { @@ -61,8 +62,9 @@ func (wm *UserV3WriteModel) Reduce() error { switch e := event.(type) { case *schemauser.CreatedEvent: wm.SchemaID = e.SchemaID - wm.SchemaRevision = 1 + wm.SchemaRevision = e.SchemaRevision wm.Data = e.Data + wm.Locked = false wm.State = domain.UserStateActive case *schemauser.UpdatedEvent: @@ -79,6 +81,8 @@ func (wm *UserV3WriteModel) Reduce() error { wm.State = domain.UserStateDeleted case *schemauser.EmailUpdatedEvent: wm.Email = string(e.EmailAddress) + wm.IsEmailVerified = false + wm.EmailVerifiedFailedCount = 0 case *schemauser.EmailCodeAddedEvent: wm.IsEmailVerified = false wm.EmailVerifiedFailedCount = 0 @@ -87,8 +91,10 @@ func (wm *UserV3WriteModel) Reduce() error { wm.EmailVerifiedFailedCount = 0 case *schemauser.EmailVerificationFailedEvent: wm.EmailVerifiedFailedCount += 1 - case *schemauser.PhoneChangedEvent: + case *schemauser.PhoneUpdatedEvent: wm.Phone = string(e.PhoneNumber) + wm.IsPhoneVerified = false + wm.PhoneVerifiedFailedCount = 0 case *schemauser.PhoneCodeAddedEvent: wm.IsPhoneVerified = false wm.PhoneVerifiedFailedCount = 0 @@ -97,28 +103,39 @@ func (wm *UserV3WriteModel) Reduce() error { wm.IsPhoneVerified = true case *schemauser.PhoneVerificationFailedEvent: wm.PhoneVerifiedFailedCount += 1 + case *schemauser.LockedEvent: + wm.Locked = true + case *schemauser.UnlockedEvent: + wm.Locked = false + case *schemauser.DeactivatedEvent: + wm.State = domain.UserStateInactive + case *schemauser.ActivatedEvent: + wm.State = domain.UserStateActive } } return wm.WriteModel.Reduce() } func (wm *UserV3WriteModel) Query() *eventstore.SearchQueryBuilder { - query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - ResourceOwner(wm.ResourceOwner). - AddQuery(). - AggregateTypes(schemauser.AggregateType). - AggregateIDs(wm.AggregateID). - EventTypes( - schemauser.CreatedType, - schemauser.DeletedType, - ) + builder := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent) + if wm.ResourceOwner != "" { + builder = builder.ResourceOwner(wm.ResourceOwner) + } + eventtypes := []eventstore.EventType{ + schemauser.CreatedType, + schemauser.DeletedType, + schemauser.ActivatedType, + schemauser.DeactivatedType, + schemauser.LockedType, + schemauser.UnlockedType, + } if wm.DataWM { - query = query.EventTypes( + eventtypes = append(eventtypes, schemauser.UpdatedType, ) } if wm.EmailWM { - query = query.EventTypes( + eventtypes = append(eventtypes, schemauser.EmailUpdatedType, schemauser.EmailVerifiedType, schemauser.EmailCodeAddedType, @@ -126,31 +143,34 @@ func (wm *UserV3WriteModel) Query() *eventstore.SearchQueryBuilder { ) } if wm.PhoneWM { - query = query.EventTypes( + eventtypes = append(eventtypes, schemauser.PhoneUpdatedType, schemauser.PhoneVerifiedType, schemauser.PhoneCodeAddedType, schemauser.PhoneVerificationFailedType, ) } - return query.Builder() + return builder.AddQuery(). + AggregateTypes(schemauser.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes(eventtypes...).Builder() } func (wm *UserV3WriteModel) NewUpdatedEvent( ctx context.Context, agg *eventstore.Aggregate, - schemaID *string, - schemaRevision *uint64, + schemaID string, + schemaRevision uint64, data json.RawMessage, ) *schemauser.UpdatedEvent { changes := make([]schemauser.Changes, 0) - if schemaID != nil && wm.SchemaID != *schemaID { - changes = append(changes, schemauser.ChangeSchemaID(wm.SchemaID, *schemaID)) + if wm.SchemaID != schemaID { + changes = append(changes, schemauser.ChangeSchemaID(schemaID)) } - if schemaRevision != nil && wm.SchemaRevision != *schemaRevision { - changes = append(changes, schemauser.ChangeSchemaRevision(wm.SchemaRevision, *schemaRevision)) + if wm.SchemaRevision != schemaRevision { + changes = append(changes, schemauser.ChangeSchemaRevision(schemaRevision)) } - if !bytes.Equal(wm.Data, data) { + if data != nil && !bytes.Equal(wm.Data, data) { changes = append(changes, schemauser.ChangeData(data)) } if len(changes) == 0 { diff --git a/internal/command/user_v3_test.go b/internal/command/user_v3_test.go index 5bbb9e0c55..69794b3c2e 100644 --- a/internal/command/user_v3_test.go +++ b/internal/command/user_v3_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -673,7 +674,7 @@ func TestCommands_CreateSchemaUser(t *testing.T) { "name": "user" }`), ), - schemauser.NewPhoneChangedEvent(context.Background(), + schemauser.NewPhoneUpdatedEvent(context.Background(), &schemauser.NewAggregate("id1", "org1").Aggregate, "+41791234567", ), @@ -751,7 +752,7 @@ func TestCommands_CreateSchemaUser(t *testing.T) { "name": "user" }`), ), - schemauser.NewPhoneChangedEvent(context.Background(), + schemauser.NewPhoneUpdatedEvent(context.Background(), &schemauser.NewAggregate("id1", "org1").Aggregate, "+41791234567", ), @@ -834,7 +835,7 @@ func TestCommands_CreateSchemaUser(t *testing.T) { schemauser.NewEmailVerifiedEvent(context.Background(), &schemauser.NewAggregate("id1", "org1").Aggregate, ), - schemauser.NewPhoneChangedEvent(context.Background(), + schemauser.NewPhoneUpdatedEvent(context.Background(), &schemauser.NewAggregate("id1", "org1").Aggregate, "+41791234567", ), @@ -904,6 +905,7 @@ func TestCommandSide_DeleteSchemaUser(t *testing.T) { type ( args struct { ctx context.Context + orgID string userID string } ) @@ -1088,7 +1090,7 @@ func TestCommandSide_DeleteSchemaUser(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } - got, err := r.DeleteSchemaUser(tt.args.ctx, tt.args.userID) + got, err := r.DeleteSchemaUser(tt.args.ctx, tt.args.orgID, tt.args.userID) if tt.res.err == nil { assert.NoError(t, err) } @@ -1101,3 +1103,2022 @@ func TestCommandSide_DeleteSchemaUser(t *testing.T) { }) } } + +func TestCommandSide_LockSchemaUser(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Eu8I2VAfjF", "Errors.IDMissing")) + }, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-G4LOrnjY7q", "Errors.User.NotFound")) + }, + }, + }, + { + name: "user removed, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewDeletedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-G4LOrnjY7q", "Errors.User.NotFound")) + }, + }, + }, + { + name: "user locked, precondition error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewLockedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-G4LOrnjY7q", "Errors.User.NotFound")) + }, + }, + }, + { + name: "lock user, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + expectPush( + schemauser.NewLockedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "lock user, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.LockSchemaUser(tt.args.ctx, tt.args.orgID, tt.args.userID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_UnlockSchemaUser(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-krXtYscQZh", "Errors.IDMissing")) + }, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-gpBv46Lh9m", "Errors.User.NotFound")) + }, + }, + }, + { + name: "user removed, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewDeletedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-gpBv46Lh9m", "Errors.User.NotFound")) + }, + }, + }, + { + name: "user not locked, precondition error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-gpBv46Lh9m", "Errors.User.NotFound")) + }, + }, + }, + { + name: "unlock user, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewLockedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + expectPush( + schemauser.NewUnlockedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "unlock user, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewLockedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.UnlockSchemaUser(tt.args.ctx, tt.args.orgID, tt.args.userID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_DeactivateSchemaUser(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-pjJhge86ZV", "Errors.IDMissing")) + }, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-Ob6lR5iFTe", "Errors.User.NotFound")) + }, + }, + }, + { + name: "user removed, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewDeletedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-Ob6lR5iFTe", "Errors.User.NotFound")) + }, + }, + }, + { + name: "user not active, precondition error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewDeactivatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-Ob6lR5iFTe", "Errors.User.NotFound")) + }, + }, + }, + { + name: "deactivate user, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + expectPush( + schemauser.NewDeactivatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "deactivate user, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.DeactivateSchemaUser(tt.args.ctx, tt.args.orgID, tt.args.userID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ReactivateSchemaUser(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-17XupGvxBJ", "Errors.IDMissing")) + }, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-rQjbBr4J3j", "Errors.User.NotFound")) + }, + }, + }, + { + name: "user removed, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewDeletedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-rQjbBr4J3j", "Errors.User.NotFound")) + }, + }, + }, + { + name: "user not inactive, precondition error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-rQjbBr4J3j", "Errors.User.NotFound")) + }, + }, + }, + { + name: "activate user, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewDeactivatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + expectPush( + schemauser.NewActivatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "activate user, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewDeactivatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.ActivateSchemaUser(tt.args.ctx, tt.args.orgID, tt.args.userID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} + +func TestCommands_ChangeSchemaUser(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + newCode encrypedCodeFunc + } + type args struct { + ctx context.Context + user *ChangeSchemaUser + } + type res struct { + returnCodeEmail string + returnCodePhone string + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no userID, error", + fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-gEJR1QOGHb", "Errors.IDMissing")) + }, + }, + }, + { + "schema not existing, error", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + SchemaID: gu.Ptr("type"), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-VLDTtxT3If", "Errors.UserSchema.NotExists")) + }, + }, + }, + { + "no valid email, error", + fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Email: &Email{Address: "noemail"}, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid")) + }, + }, + }, + { + "no valid phone, error", + fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Phone: &Phone{Number: "invalid"}, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "PHONE-so0wa", "Errors.User.Phone.Invalid")) + }, + }, + }, + { + "user update, no permission", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "id1", + 1, + json.RawMessage(`{ + "name": "user1" + }`), + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Data: json.RawMessage(`{ + "name": "user" + }`), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + "user updated, same 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( + 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}, + ), + ), + ), + expectPush( + schemauser.NewUpdatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + []schemauser.Changes{ + schemauser.ChangeData( + json.RawMessage(`{ + "name": "user2" + }`), + ), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Data: json.RawMessage(`{ + "name": "user2" + }`), + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user updated, changed schema", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id2", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "id1", + 1, + json.RawMessage(`{ + "name": "user1" + }`), + ), + ), + ), + expectPush( + schemauser.NewUpdatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + []schemauser.Changes{ + schemauser.ChangeSchemaID("id2"), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + SchemaID: gu.Ptr("id2"), + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user updated, new schema", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id2", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "id1", + 1, + json.RawMessage(`{ + "name": "user1" + }`), + ), + ), + ), + expectPush( + schemauser.NewUpdatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + []schemauser.Changes{ + schemauser.ChangeSchemaID("id2"), + schemauser.ChangeData( + json.RawMessage(`{ + "name": "user2" + }`), + ), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + SchemaID: gu.Ptr("id2"), + Data: json.RawMessage(`{ + "name": "user2" + }`), + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user updated, same schema revision", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "id1", + 1, + json.RawMessage(`{ + "name1": "user1" + }`), + ), + ), + ), + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name1": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + eventFromEventPusher( + schema.NewUpdatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + []schema.Changes{ + schema.IncreaseRevision(1), + schema.ChangeSchema(json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name2": { + "type": "string" + } + } + }`)), + }, + ), + ), + ), + expectPush( + schemauser.NewUpdatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + []schemauser.Changes{ + schemauser.ChangeSchemaRevision(2), + schemauser.ChangeData( + json.RawMessage(`{ + "name2": "user2" + }`), + ), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Data: json.RawMessage(`{ + "name2": "user2" + }`), + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user updated, new schema and revision", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id2", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name2": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "id1", + 2, + json.RawMessage(`{ + "name1": "user1" + }`), + ), + ), + ), + expectPush( + schemauser.NewUpdatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + []schemauser.Changes{ + schemauser.ChangeSchemaID("id2"), + schemauser.ChangeSchemaRevision(1), + schemauser.ChangeData( + json.RawMessage(`{ + "name2": "user2" + }`), + ), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + SchemaID: gu.Ptr("id2"), + Data: json.RawMessage(`{ + "name2": "user2" + }`), + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user update, no field permission as admin", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "urn:zitadel:schema:permission": { + "owner": "r" + }, + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + 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(`{ + "name": "user" + }`), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")) + }, + }, + }, + { + "user update, no field permission as user", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "urn:zitadel:schema:permission": { + "self": "r" + }, + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + 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(`{ + "name": "user" + }`), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")) + }, + }, + }, + { + "user update, invalid data type", + fields{ + eventstore: expectEventstore( + 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}, + ), + ), + ), + 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(`{ + "name": 1 + }`), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")) + }, + }, + }, + { + "user update, additional property", + fields{ + eventstore: expectEventstore( + 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}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "id1", + 1, + json.RawMessage(`{ + "name": "user1" + }`), + ), + ), + ), + expectPush( + schemauser.NewUpdatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + []schemauser.Changes{ + schemauser.ChangeData( + json.RawMessage(`{ + "name": "user1", + "additional": "property" + }`), + ), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + SchemaID: gu.Ptr("id1"), + Data: json.RawMessage(`{ + "name": "user1", + "additional": "property" + }`), + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user update, invalid data attribute name", + fields{ + eventstore: expectEventstore( + 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" + } + }, + "additionalProperties": false + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + 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(`{ + "invalid": "user" + }`), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")) + }, + }, + }, + { + "user update, email not changed", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "id1", + 1, + json.RawMessage(`{ + "name": "user1" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewEmailUpdatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "test@example.com", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Email: &Email{ + Address: "test@example.com", + ReturnCode: true, + URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user update, email return", + fields{ + eventstore: expectEventstore( + 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}, + ), + ), + ), + 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, + "test@example.com", + ), + schemauser.NewEmailCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("emailverify"), + }, + time.Hour*1, + "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("emailverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + SchemaID: gu.Ptr("id1"), + Email: &Email{ + Address: "test@example.com", + ReturnCode: true, + URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + returnCodeEmail: "emailverify", + }, + }, + { + "user updated, email to verify", + fields{ + eventstore: expectEventstore( + expectFilter( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + )), + expectPush( + schemauser.NewEmailUpdatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "test@example.com", + ), + schemauser.NewEmailCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("emailverify"), + }, + time.Hour*1, + "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("emailverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Email: &Email{ + Address: "test@example.com", + URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user updated, phone no change", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewPhoneUpdatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "+41791234567", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Phone: &Phone{ + Number: "+41791234567", + ReturnCode: true, + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user updated, phone return", + fields{ + eventstore: expectEventstore( + expectFilter( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + expectPush( + schemauser.NewPhoneUpdatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("phoneverify"), + }, + time.Hour*1, + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("phoneverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Phone: &Phone{ + Number: "+41791234567", + ReturnCode: true, + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + returnCodePhone: "phoneverify", + }, + }, + { + "user updated, phone to verify", + fields{ + eventstore: expectEventstore( + expectFilter( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + expectPush( + schemauser.NewPhoneUpdatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("phoneverify"), + }, + time.Hour*1, + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("phoneverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Phone: &Phone{ + Number: "+41791234567", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "user updated, full verified", + fields{ + eventstore: expectEventstore( + expectFilter( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + expectPush( + schemauser.NewEmailUpdatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "test@example.com", + ), + schemauser.NewEmailVerifiedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + schemauser.NewPhoneUpdatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneVerifiedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &ChangeSchemaUser{ + ID: "user1", + Email: &Email{Address: "test@example.com", Verified: true}, + Phone: &Phone{Number: "+41791234567", Verified: true}, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + newEncryptedCode: tt.fields.newCode, + } + err := c.ChangeSchemaUser(tt.args.ctx, tt.args.user, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.details, tt.args.user.Details) + } + + if tt.res.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) + } + }) + } +} diff --git a/internal/integration/client.go b/internal/integration/client.go index 82b8ab3b6e..9bf855f5ce 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -28,7 +28,7 @@ import ( object_v3alpha "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" - org "github.com/zitadel/zitadel/pkg/grpc/org/v2" + "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" @@ -784,3 +784,55 @@ func (i *Instance) CreateInviteCode(ctx context.Context, userID string) *user_v2 logging.OnError(err).Fatal("create invite code") return user } + +func (i *Instance) LockSchemaUser(ctx context.Context, orgID string, userID string) *user_v3alpha.LockUserResponse { + var org *object_v3alpha.Organization + if orgID != "" { + org = &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}} + } + user, err := i.Client.UserV3Alpha.LockUser(ctx, &user_v3alpha.LockUserRequest{ + Organization: org, + Id: userID, + }) + logging.OnError(err).Fatal("lock user") + return user +} + +func (i *Instance) UnlockSchemaUser(ctx context.Context, orgID string, userID string) *user_v3alpha.UnlockUserResponse { + var org *object_v3alpha.Organization + if orgID != "" { + org = &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}} + } + user, err := i.Client.UserV3Alpha.UnlockUser(ctx, &user_v3alpha.UnlockUserRequest{ + Organization: org, + Id: userID, + }) + logging.OnError(err).Fatal("unlock user") + return user +} + +func (i *Instance) DeactivateSchemaUser(ctx context.Context, orgID string, userID string) *user_v3alpha.DeactivateUserResponse { + var org *object_v3alpha.Organization + if orgID != "" { + org = &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}} + } + user, err := i.Client.UserV3Alpha.DeactivateUser(ctx, &user_v3alpha.DeactivateUserRequest{ + Organization: org, + Id: userID, + }) + logging.OnError(err).Fatal("deactivate user") + return user +} + +func (i *Instance) ActivateSchemaUser(ctx context.Context, orgID string, userID string) *user_v3alpha.ActivateUserResponse { + var org *object_v3alpha.Organization + if orgID != "" { + org = &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}} + } + user, err := i.Client.UserV3Alpha.ActivateUser(ctx, &user_v3alpha.ActivateUserRequest{ + Organization: org, + Id: userID, + }) + logging.OnError(err).Fatal("reactivate user") + return user +} diff --git a/internal/repository/user/schemauser/aggregate.go b/internal/repository/user/schemauser/aggregate.go index 1c9901c08c..18b901281e 100644 --- a/internal/repository/user/schemauser/aggregate.go +++ b/internal/repository/user/schemauser/aggregate.go @@ -5,8 +5,8 @@ import ( ) const ( - AggregateType = "user" - AggregateVersion = "v3" + AggregateType = "schemauser" + AggregateVersion = "v1" ) type Aggregate struct { diff --git a/internal/repository/user/schemauser/email.go b/internal/repository/user/schemauser/email.go index 07ae1bdf71..b5e4206252 100644 --- a/internal/repository/user/schemauser/email.go +++ b/internal/repository/user/schemauser/email.go @@ -8,7 +8,6 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/zerrors" ) const ( @@ -21,11 +20,15 @@ const ( ) type EmailUpdatedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` EmailAddress domain.EmailAddress `json:"email,omitempty"` } +func (e *EmailUpdatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *EmailUpdatedEvent) Payload() interface{} { return e } @@ -36,7 +39,7 @@ func (e *EmailUpdatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { func NewEmailUpdatedEvent(ctx context.Context, aggregate *eventstore.Aggregate, emailAddress domain.EmailAddress) *EmailUpdatedEvent { return &EmailUpdatedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, EmailUpdatedType, @@ -45,24 +48,16 @@ func NewEmailUpdatedEvent(ctx context.Context, aggregate *eventstore.Aggregate, } } -func EmailUpdatedEventMapper(event eventstore.Event) (eventstore.Event, error) { - emailChangedEvent := &EmailUpdatedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(emailChangedEvent) - if err != nil { - return nil, zerrors.ThrowInternal(err, "USER-4M0sd", "unable to unmarshal human password changed") - } - - return emailChangedEvent, nil -} - type EmailVerifiedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` IsEmailVerified bool `json:"-"` } +func (e *EmailVerifiedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *EmailVerifiedEvent) Payload() interface{} { return nil } @@ -73,7 +68,7 @@ func (e *EmailVerifiedEvent) UniqueConstraints() []*eventstore.UniqueConstraint func NewEmailVerifiedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailVerifiedEvent { return &EmailVerifiedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, EmailVerifiedType, @@ -81,18 +76,13 @@ func NewEmailVerifiedEvent(ctx context.Context, aggregate *eventstore.Aggregate) } } -func HumanVerifiedEventMapper(event eventstore.Event) (eventstore.Event, error) { - emailVerified := &EmailVerifiedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - IsEmailVerified: true, - } - return emailVerified, nil -} - type EmailVerificationFailedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` } +func (e *EmailVerificationFailedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} func (e *EmailVerificationFailedEvent) Payload() interface{} { return nil } @@ -101,9 +91,9 @@ func (e *EmailVerificationFailedEvent) UniqueConstraints() []*eventstore.UniqueC return nil } -func NewHumanEmailVerificationFailedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailVerificationFailedEvent { +func NewEmailVerificationFailedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailVerificationFailedEvent { return &EmailVerificationFailedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, EmailVerificationFailedType, @@ -111,14 +101,8 @@ func NewHumanEmailVerificationFailedEvent(ctx context.Context, aggregate *events } } -func EmailVerificationFailedEventMapper(event eventstore.Event) (eventstore.Event, error) { - return &EmailVerificationFailedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - }, nil -} - type EmailCodeAddedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` Code *crypto.CryptoValue `json:"code,omitempty"` Expiry time.Duration `json:"expiry,omitempty"` @@ -127,6 +111,10 @@ type EmailCodeAddedEvent struct { TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` } +func (e *EmailCodeAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *EmailCodeAddedEvent) Payload() interface{} { return e } @@ -148,7 +136,7 @@ func NewEmailCodeAddedEvent( codeReturned bool, ) *EmailCodeAddedEvent { return &EmailCodeAddedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, EmailCodeAddedType, @@ -161,22 +149,13 @@ func NewEmailCodeAddedEvent( } } -func EmailCodeAddedEventMapper(event eventstore.Event) (eventstore.Event, error) { - codeAdded := &EmailCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(codeAdded) - if err != nil { - return nil, zerrors.ThrowInternal(err, "USER-3M0sd", "unable to unmarshal human email code added") - } - - return codeAdded, nil -} - type EmailCodeSentEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` } +func (e *EmailCodeSentEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} func (e *EmailCodeSentEvent) Payload() interface{} { return nil } @@ -185,18 +164,12 @@ func (e *EmailCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint return nil } -func NewHumanEmailCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailCodeSentEvent { +func NewEmailCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailCodeSentEvent { return &EmailCodeSentEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, EmailCodeSentType, ), } } - -func EmailCodeSentEventMapper(event eventstore.Event) (eventstore.Event, error) { - return &EmailCodeSentEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - }, nil -} diff --git a/internal/repository/user/schemauser/eventstore.go b/internal/repository/user/schemauser/eventstore.go index b9cf03e5d3..85ad15c17d 100644 --- a/internal/repository/user/schemauser/eventstore.go +++ b/internal/repository/user/schemauser/eventstore.go @@ -6,4 +6,18 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, CreatedType, eventstore.GenericEventMapper[CreatedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, UpdatedType, eventstore.GenericEventMapper[UpdatedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, DeletedType, eventstore.GenericEventMapper[DeletedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, LockedType, eventstore.GenericEventMapper[LockedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, UnlockedType, eventstore.GenericEventMapper[UnlockedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, ActivatedType, eventstore.GenericEventMapper[ActivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, DeactivatedType, eventstore.GenericEventMapper[DeactivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, EmailUpdatedType, eventstore.GenericEventMapper[EmailUpdatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, EmailCodeAddedType, eventstore.GenericEventMapper[EmailCodeAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, EmailCodeSentType, eventstore.GenericEventMapper[EmailCodeSentEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, EmailVerifiedType, eventstore.GenericEventMapper[EmailVerifiedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, EmailVerificationFailedType, eventstore.GenericEventMapper[EmailVerificationFailedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, PhoneUpdatedType, eventstore.GenericEventMapper[PhoneUpdatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, PhoneCodeAddedType, eventstore.GenericEventMapper[PhoneCodeAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, PhoneCodeSentType, eventstore.GenericEventMapper[PhoneCodeSentEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, PhoneVerifiedType, eventstore.GenericEventMapper[PhoneVerifiedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, PhoneVerificationFailedType, eventstore.GenericEventMapper[PhoneVerificationFailedEvent]) } diff --git a/internal/repository/user/schemauser/phone.go b/internal/repository/user/schemauser/phone.go index 5110772c04..a491dab776 100644 --- a/internal/repository/user/schemauser/phone.go +++ b/internal/repository/user/schemauser/phone.go @@ -8,7 +8,6 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/zerrors" ) const ( @@ -20,23 +19,27 @@ const ( PhoneCodeSentType = phoneEventPrefix + "code.sent" ) -type PhoneChangedEvent struct { - eventstore.BaseEvent `json:"-"` +type PhoneUpdatedEvent struct { + *eventstore.BaseEvent `json:"-"` PhoneNumber domain.PhoneNumber `json:"phone,omitempty"` } -func (e *PhoneChangedEvent) Payload() interface{} { +func (e *PhoneUpdatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *PhoneUpdatedEvent) Payload() interface{} { return e } -func (e *PhoneChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { +func (e *PhoneUpdatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { return nil } -func NewPhoneChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, phone domain.PhoneNumber) *PhoneChangedEvent { - return &PhoneChangedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( +func NewPhoneUpdatedEvent(ctx context.Context, aggregate *eventstore.Aggregate, phone domain.PhoneNumber) *PhoneUpdatedEvent { + return &PhoneUpdatedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, PhoneUpdatedType, @@ -45,24 +48,15 @@ func NewPhoneChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, } } -func PhoneChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { - phoneChangedEvent := &PhoneChangedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(phoneChangedEvent) - if err != nil { - return nil, zerrors.ThrowInternal(err, "USER-5M0pd", "unable to unmarshal phone changed") - } - - return phoneChangedEvent, nil -} - type PhoneVerifiedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` IsPhoneVerified bool `json:"-"` } +func (e *PhoneVerifiedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} func (e *PhoneVerifiedEvent) Payload() interface{} { return nil } @@ -73,7 +67,7 @@ func (e *PhoneVerifiedEvent) UniqueConstraints() []*eventstore.UniqueConstraint func NewPhoneVerifiedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneVerifiedEvent { return &PhoneVerifiedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, PhoneVerifiedType, @@ -81,15 +75,12 @@ func NewPhoneVerifiedEvent(ctx context.Context, aggregate *eventstore.Aggregate) } } -func PhoneVerifiedEventMapper(event eventstore.Event) (eventstore.Event, error) { - return &PhoneVerifiedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - IsPhoneVerified: true, - }, nil +type PhoneVerificationFailedEvent struct { + *eventstore.BaseEvent `json:"-"` } -type PhoneVerificationFailedEvent struct { - eventstore.BaseEvent `json:"-"` +func (e *PhoneVerificationFailedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event } func (e *PhoneVerificationFailedEvent) Payload() interface{} { @@ -102,7 +93,7 @@ func (e *PhoneVerificationFailedEvent) UniqueConstraints() []*eventstore.UniqueC func NewPhoneVerificationFailedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneVerificationFailedEvent { return &PhoneVerificationFailedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, PhoneVerificationFailedType, @@ -110,14 +101,8 @@ func NewPhoneVerificationFailedEvent(ctx context.Context, aggregate *eventstore. } } -func PhoneVerificationFailedEventMapper(event eventstore.Event) (eventstore.Event, error) { - return &PhoneVerificationFailedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - }, nil -} - type PhoneCodeAddedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` Code *crypto.CryptoValue `json:"code,omitempty"` Expiry time.Duration `json:"expiry,omitempty"` @@ -137,6 +122,10 @@ func (e *PhoneCodeAddedEvent) TriggerOrigin() string { return e.TriggeredAtOrigin } +func (e *PhoneCodeAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func NewPhoneCodeAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, @@ -145,7 +134,7 @@ func NewPhoneCodeAddedEvent( codeReturned bool, ) *PhoneCodeAddedEvent { return &PhoneCodeAddedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, PhoneCodeAddedType, @@ -157,20 +146,8 @@ func NewPhoneCodeAddedEvent( } } -func PhoneCodeAddedEventMapper(event eventstore.Event) (eventstore.Event, error) { - codeAdded := &PhoneCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(codeAdded) - if err != nil { - return nil, zerrors.ThrowInternal(err, "USER-6Ms9d", "unable to unmarshal phone code added") - } - - return codeAdded, nil -} - type PhoneCodeSentEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` } func (e *PhoneCodeSentEvent) Payload() interface{} { @@ -181,18 +158,16 @@ func (e *PhoneCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint return nil } +func (e *PhoneCodeSentEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func NewPhoneCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneCodeSentEvent { return &PhoneCodeSentEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, PhoneCodeSentType, ), } } - -func PhoneCodeSentEventMapper(event eventstore.Event) (eventstore.Event, error) { - return &PhoneCodeSentEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - }, nil -} diff --git a/internal/repository/user/schemauser/user.go b/internal/repository/user/schemauser/user.go index 4c88d53087..6583e57366 100644 --- a/internal/repository/user/schemauser/user.go +++ b/internal/repository/user/schemauser/user.go @@ -8,10 +8,14 @@ import ( ) const ( - eventPrefix = "user." - CreatedType = eventPrefix + "created" - UpdatedType = eventPrefix + "updated" - DeletedType = eventPrefix + "deleted" + eventPrefix = "schemauser." + CreatedType = eventPrefix + "created" + UpdatedType = eventPrefix + "updated" + DeletedType = eventPrefix + "deleted" + LockedType = eventPrefix + "locked" + UnlockedType = eventPrefix + "unlocked" + DeactivatedType = eventPrefix + "deactivated" + ActivatedType = eventPrefix + "activated" ) type CreatedEvent struct { @@ -60,8 +64,6 @@ type UpdatedEvent struct { SchemaID *string `json:"schemaID,omitempty"` SchemaRevision *uint64 `json:"schemaRevision,omitempty"` Data json.RawMessage `json:"schema,omitempty"` - oldSchemaID string - oldRevision uint64 } func (e *UpdatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { @@ -95,16 +97,14 @@ func NewUpdatedEvent( type Changes func(event *UpdatedEvent) -func ChangeSchemaID(oldSchemaID, schemaID string) func(event *UpdatedEvent) { +func ChangeSchemaID(schemaID string) func(event *UpdatedEvent) { return func(e *UpdatedEvent) { e.SchemaID = &schemaID - e.oldSchemaID = oldSchemaID } } -func ChangeSchemaRevision(oldSchemaRevision, schemaRevision uint64) func(event *UpdatedEvent) { +func ChangeSchemaRevision(schemaRevision uint64) func(event *UpdatedEvent) { return func(e *UpdatedEvent) { e.SchemaRevision = &schemaRevision - e.oldRevision = oldSchemaRevision } } @@ -142,3 +142,119 @@ func NewDeletedEvent( ), } } + +type LockedEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *LockedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *LockedEvent) Payload() interface{} { + return e +} + +func (e *LockedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewLockedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *LockedEvent { + return &LockedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + LockedType, + ), + } +} + +type UnlockedEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *UnlockedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *UnlockedEvent) Payload() interface{} { + return e +} + +func (e *UnlockedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewUnlockedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *UnlockedEvent { + return &UnlockedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + UnlockedType, + ), + } +} + +type DeactivatedEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *DeactivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *DeactivatedEvent) Payload() interface{} { + return e +} + +func (e *DeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewDeactivatedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *DeactivatedEvent { + return &DeactivatedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + DeactivatedType, + ), + } +} + +type ActivatedEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *ActivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *ActivatedEvent) Payload() interface{} { + return e +} + +func (e *ActivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewActivatedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *ActivatedEvent { + return &ActivatedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + ActivatedType, + ), + } +} diff --git a/proto/zitadel/resources/user/v3alpha/user_service.proto b/proto/zitadel/resources/user/v3alpha/user_service.proto index d39ac1930a..91831bdc40 100644 --- a/proto/zitadel/resources/user/v3alpha/user_service.proto +++ b/proto/zitadel/resources/user/v3alpha/user_service.proto @@ -150,7 +150,7 @@ service ZITADELUsers { // Returns the user identified by the requested ID. rpc GetUser (GetUserRequest) returns (GetUserResponse) { option (google.api.http) = { - get: "/resources/v3alpha/users/{user_id}" + get: "/resources/v3alpha/users/{id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -208,7 +208,7 @@ service ZITADELUsers { // Patch an existing user with data based on a user schema. rpc PatchUser (PatchUserRequest) returns (PatchUserResponse) { option (google.api.http) = { - patch: "/resources/v3alpha/users/{user_id}" + patch: "/resources/v3alpha/users/{id}" body: "user" }; @@ -238,7 +238,7 @@ service ZITADELUsers { // The endpoint returns an error if the user is already in the state 'deactivated'. rpc DeactivateUser (DeactivateUserRequest) returns (DeactivateUserResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/_deactivate" + post: "/resources/v3alpha/users/{id}/_deactivate" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -257,15 +257,15 @@ service ZITADELUsers { }; } - // Reactivate a user + // Activate a user // - // Reactivate a previously deactivated user and change the state to 'active'. + // Activate a previously deactivated user and change the state to 'active'. // The user will be able to log in again. // // The endpoint returns an error if the user is not in the state 'deactivated'. - rpc ReactivateUser (ReactivateUserRequest) returns (ReactivateUserResponse) { + rpc ActivateUser (ActivateUserRequest) returns (ActivateUserResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/_reactivate" + post: "/resources/v3alpha/users/{id}/_activate" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -278,7 +278,7 @@ service ZITADELUsers { responses: { key: "200"; value: { - description: "User successfully reactivated"; + description: "User successfully activated"; }; }; }; @@ -294,7 +294,7 @@ service ZITADELUsers { // The endpoint returns an error if the user is already in the state 'locked'. rpc LockUser (LockUserRequest) returns (LockUserResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/_lock" + post: "/resources/v3alpha/users/{id}/_lock" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -321,7 +321,7 @@ service ZITADELUsers { // The endpoint returns an error if the user is not in the state 'locked'. rpc UnlockUser (UnlockUserRequest) returns (UnlockUserResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/_unlock" + post: "/resources/v3alpha/users/{id}/_unlock" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -346,7 +346,7 @@ service ZITADELUsers { // The user will be able to log in anymore. rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse) { option (google.api.http) = { - delete: "/resources/v3alpha/users/{user_id}" + delete: "/resources/v3alpha/users/{id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -372,7 +372,7 @@ service ZITADELUsers { // which can be either returned or will be sent to the user by email. rpc SetContactEmail (SetContactEmailRequest) returns (SetContactEmailResponse) { option (google.api.http) = { - put: "/resources/v3alpha/users/{user_id}/email" + put: "/resources/v3alpha/users/{id}/email" body: "email" }; @@ -397,7 +397,7 @@ service ZITADELUsers { // Verify the contact email with the provided code. rpc VerifyContactEmail (VerifyContactEmailRequest) returns (VerifyContactEmailResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/email/_verify" + post: "/resources/v3alpha/users/{id}/email/_verify" body: "verification_code" }; @@ -422,7 +422,7 @@ service ZITADELUsers { // Resend the email with the verification code for the contact email address. rpc ResendContactEmailCode (ResendContactEmailCodeRequest) returns (ResendContactEmailCodeResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/email/_resend" + post: "/resources/v3alpha/users/{id}/email/_resend" body: "*" }; @@ -449,7 +449,7 @@ service ZITADELUsers { // which can be either returned or will be sent to the user by SMS. rpc SetContactPhone (SetContactPhoneRequest) returns (SetContactPhoneResponse) { option (google.api.http) = { - put: "/resources/v3alpha/users/{user_id}/phone" + put: "/resources/v3alpha/users/{id}/phone" body: "phone" }; @@ -474,7 +474,7 @@ service ZITADELUsers { // Verify the contact phone with the provided code. rpc VerifyContactPhone (VerifyContactPhoneRequest) returns (VerifyContactPhoneResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/phone/_verify" + post: "/resources/v3alpha/users/{id}/phone/_verify" body: "verification_code" }; @@ -499,7 +499,7 @@ service ZITADELUsers { // Resend the phone with the verification code for the contact phone number. rpc ResendContactPhoneCode (ResendContactPhoneCodeRequest) returns (ResendContactPhoneCodeResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/phone/_resend" + post: "/resources/v3alpha/users/{id}/phone/_resend" body: "*" }; @@ -524,7 +524,7 @@ service ZITADELUsers { // Add a new unique username to a user. The username will be used to identify the user on authentication. rpc AddUsername (AddUsernameRequest) returns (AddUsernameResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/username" + post: "/resources/v3alpha/users/{id}/username" body: "username" }; @@ -549,7 +549,7 @@ service ZITADELUsers { // Remove an existing username of a user, so it cannot be used for authentication anymore. rpc RemoveUsername (RemoveUsernameRequest) returns (RemoveUsernameResponse) { option (google.api.http) = { - delete: "/resources/v3alpha/users/{user_id}/username/{username_id}" + delete: "/resources/v3alpha/users/{id}/username/{username_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -573,7 +573,7 @@ service ZITADELUsers { // Add, update or reset a user's password with either a verification code or the current password. rpc SetPassword (SetPasswordRequest) returns (SetPasswordResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/password" + post: "/resources/v3alpha/users/{id}/password" body: "new_password" }; @@ -598,7 +598,7 @@ service ZITADELUsers { // Request a code to be able to set a new password. rpc RequestPasswordReset (RequestPasswordResetRequest) returns (RequestPasswordResetResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/password/_reset" + post: "/resources/v3alpha/users/{id}/password/_reset" body: "*" }; @@ -625,7 +625,7 @@ service ZITADELUsers { // which are used to verify the device. rpc StartWebAuthNRegistration (StartWebAuthNRegistrationRequest) returns (StartWebAuthNRegistrationResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/webauthn" + post: "/resources/v3alpha/users/{id}/webauthn" body: "registration" }; @@ -650,7 +650,7 @@ service ZITADELUsers { // Verify the WebAuthN registration started by StartWebAuthNRegistration with the public key credential. rpc VerifyWebAuthNRegistration (VerifyWebAuthNRegistrationRequest) returns (VerifyWebAuthNRegistrationResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/webauthn/{web_auth_n_id}/_verify" + post: "/resources/v3alpha/users/{id}/webauthn/{web_auth_n_id}/_verify" body: "verify" }; @@ -675,7 +675,7 @@ service ZITADELUsers { // The code will allow the user to start a new WebAuthN registration. rpc CreateWebAuthNRegistrationLink (CreateWebAuthNRegistrationLinkRequest) returns (CreateWebAuthNRegistrationLinkResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/webauthn/registration_link" + post: "/resources/v3alpha/users/{id}/webauthn/registration_link" body: "*" }; @@ -699,7 +699,7 @@ service ZITADELUsers { // Remove an existing WebAuthN authenticator from a user, so it cannot be used for authentication anymore. rpc RemoveWebAuthNAuthenticator (RemoveWebAuthNAuthenticatorRequest) returns (RemoveWebAuthNAuthenticatorResponse) { option (google.api.http) = { - delete: "/resources/v3alpha/users/{user_id}/webauthn/{web_auth_n_id}" + delete: "/resources/v3alpha/users/{id}/webauthn/{web_auth_n_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -723,7 +723,7 @@ service ZITADELUsers { // As a response a secret is returned, which is used to initialize a TOTP app or device. rpc StartTOTPRegistration (StartTOTPRegistrationRequest) returns (StartTOTPRegistrationResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/totp" + post: "/resources/v3alpha/users/{id}/totp" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -746,7 +746,7 @@ service ZITADELUsers { // Verify the time-based one-time-password (TOTP) registration with the generated code. rpc VerifyTOTPRegistration (VerifyTOTPRegistrationRequest) returns (VerifyTOTPRegistrationResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/totp/{totp_id}/_verify" + post: "/resources/v3alpha/users/{id}/totp/{totp_id}/_verify" body: "code" }; @@ -770,7 +770,7 @@ service ZITADELUsers { // Remove an existing time-based one-time-password (TOTP) authenticator from a user, so it cannot be used for authentication anymore. rpc RemoveTOTPAuthenticator (RemoveTOTPAuthenticatorRequest) returns (RemoveTOTPAuthenticatorResponse) { option (google.api.http) = { - delete: "/resources/v3alpha/users/{user_id}/totp/{totp_id}" + delete: "/resources/v3alpha/users/{id}/totp/{totp_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -795,7 +795,7 @@ service ZITADELUsers { // which can be either returned or will be sent to the user by SMS. rpc AddOTPSMSAuthenticator (AddOTPSMSAuthenticatorRequest) returns (AddOTPSMSAuthenticatorResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/otp_sms" + post: "/resources/v3alpha/users/{id}/otp_sms" body: "phone" }; @@ -819,7 +819,7 @@ service ZITADELUsers { // Verify the OTP SMS registration with the provided code. rpc VerifyOTPSMSRegistration (VerifyOTPSMSRegistrationRequest) returns (VerifyOTPSMSRegistrationResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/otp_sms/{otp_sms_id}/_verify" + post: "/resources/v3alpha/users/{id}/otp_sms/{otp_sms_id}/_verify" body: "code" }; @@ -844,7 +844,7 @@ service ZITADELUsers { // Remove an existing one-time-password (OTP) SMS authenticator from a user, so it cannot be used for authentication anymore. rpc RemoveOTPSMSAuthenticator (RemoveOTPSMSAuthenticatorRequest) returns (RemoveOTPSMSAuthenticatorResponse) { option (google.api.http) = { - delete: "/resources/v3alpha/users/{user_id}/otp_sms/{otp_sms_id}" + delete: "/resources/v3alpha/users/{id}/otp_sms/{otp_sms_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -869,7 +869,7 @@ service ZITADELUsers { // which can be either returned or will be sent to the user by email. rpc AddOTPEmailAuthenticator (AddOTPEmailAuthenticatorRequest) returns (AddOTPEmailAuthenticatorResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/otp_email" + post: "/resources/v3alpha/users/{id}/otp_email" body: "email" }; @@ -893,7 +893,7 @@ service ZITADELUsers { // Verify the OTP Email registration with the provided code. rpc VerifyOTPEmailRegistration (VerifyOTPEmailRegistrationRequest) returns (VerifyOTPEmailRegistrationResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/otp_email/{otp_email_id}/_verify" + post: "/resources/v3alpha/users/{id}/otp_email/{otp_email_id}/_verify" body: "code" }; @@ -918,7 +918,7 @@ service ZITADELUsers { // Remove an existing one-time-password (OTP) Email authenticator from a user, so it cannot be used for authentication anymore. rpc RemoveOTPEmailAuthenticator (RemoveOTPEmailAuthenticatorRequest) returns (RemoveOTPEmailAuthenticatorResponse) { option (google.api.http) = { - delete: "/resources/v3alpha/users/{user_id}/otp_email/{otp_email_id}" + delete: "/resources/v3alpha/users/{id}/otp_email/{otp_email_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -991,7 +991,7 @@ service ZITADELUsers { // This will allow the user to authenticate with the provided IDP. rpc AddIDPAuthenticator (AddIDPAuthenticatorRequest) returns (AddIDPAuthenticatorResponse) { option (google.api.http) = { - post: "/resources/v3alpha/users/{user_id}/idps" + post: "/resources/v3alpha/users/{id}/idps" body: "authenticator" }; @@ -1016,7 +1016,7 @@ service ZITADELUsers { // Remove an existing identity provider (IDP) authenticator from a user, so it cannot be used for authentication anymore. rpc RemoveIDPAuthenticator (RemoveIDPAuthenticatorRequest) returns (RemoveIDPAuthenticatorResponse) { option (google.api.http) = { - delete: "/resources/v3alpha/users/{user_id}/idps/{idp_id}" + delete: "/resources/v3alpha/users/{id}/idps/{idp_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -1069,7 +1069,7 @@ message GetUserRequest { } ]; // unique identifier of the user. - string user_id = 2 [ + string id = 2 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1123,7 +1123,7 @@ message PatchUserRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"69629012906488334\""; } @@ -1156,7 +1156,7 @@ message DeactivateUserRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1172,7 +1172,7 @@ message DeactivateUserResponse { } -message ReactivateUserRequest { +message ActivateUserRequest { optional zitadel.object.v3alpha.Instance instance = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { default: "\"domain from HOST or :authority header\"" @@ -1181,7 +1181,7 @@ message ReactivateUserRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1192,7 +1192,7 @@ message ReactivateUserRequest { ]; } -message ReactivateUserResponse { +message ActivateUserResponse { zitadel.resources.object.v3alpha.Details details = 1; } @@ -1205,7 +1205,7 @@ message LockUserRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1229,7 +1229,7 @@ message UnlockUserRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1253,7 +1253,7 @@ message DeleteUserRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1277,7 +1277,7 @@ message SetContactEmailRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1309,7 +1309,7 @@ message VerifyContactEmailRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1343,7 +1343,7 @@ message ResendContactEmailCodeRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1376,7 +1376,7 @@ message SetContactPhoneRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1408,7 +1408,7 @@ message VerifyContactPhoneRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1442,7 +1442,7 @@ message ResendContactPhoneCodeRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1475,7 +1475,7 @@ message AddUsernameRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1507,7 +1507,7 @@ message RemoveUsernameRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1541,7 +1541,7 @@ message SetPasswordRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1567,7 +1567,7 @@ message RequestPasswordResetRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1606,7 +1606,7 @@ message StartWebAuthNRegistrationRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1645,7 +1645,7 @@ message VerifyWebAuthNRegistrationRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1680,7 +1680,7 @@ message CreateWebAuthNRegistrationLinkRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1713,7 +1713,7 @@ message RemoveWebAuthNAuthenticatorRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1747,7 +1747,7 @@ message StartTOTPRegistrationRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1789,7 +1789,7 @@ message VerifyTOTPRegistrationRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1833,7 +1833,7 @@ message RemoveTOTPAuthenticatorRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1867,7 +1867,7 @@ message AddOTPSMSAuthenticatorRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1906,7 +1906,7 @@ message VerifyOTPSMSRegistrationRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1950,7 +1950,7 @@ message RemoveOTPSMSAuthenticatorRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1984,7 +1984,7 @@ message AddOTPEmailAuthenticatorRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2022,7 +2022,7 @@ message VerifyOTPEmailRegistrationRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2066,7 +2066,7 @@ message RemoveOTPEmailAuthenticatorRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2170,7 +2170,7 @@ message GetIdentityProviderIntentResponse { // and detailed / profile information. IDPInformation idp_information = 2; // If the user was already federated and linked to a ZITADEL user, it's id will be returned. - optional string user_id = 3 [ + optional string id = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"163840776835432345\""; } @@ -2186,7 +2186,7 @@ message AddIDPAuthenticatorRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2211,7 +2211,7 @@ message RemoveIDPAuthenticatorRequest { // Optionally expect the user to be in this organization. optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 3 [ + string id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { From 77aa02a5216430bddc1da99a6596b328ea2befad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 17 Sep 2024 13:08:13 +0300 Subject: [PATCH 02/16] fix(projection): increase transaction duration (#8632) # Which Problems Are Solved Reduce the chance for projection dead-locks. Increasing or disabling the projection transaction duration solved dead-locks in all reported cases. # How the Problems Are Solved Increase the default transaction duration to 1 minute. Due to the high value it is functionally similar to disabling, however it still provides a safety net for transaction that do freeze, perhaps due to connection issues with the database. # Additional Changes - Integration test uses default. - Technical advisory # Additional Context - Related to https://github.com/zitadel/zitadel/issues/8517 --------- Co-authored-by: Silvan --- cmd/defaults.yaml | 5 +- docs/docs/support/advisory/a10012.md | 60 ++++++++++++++++++++++++ internal/integration/config/zitadel.yaml | 1 - 3 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 docs/docs/support/advisory/a10012.md diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index a81a1ff126..5da59fa0d0 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -231,7 +231,7 @@ Projections: # The maximum duration a transaction remains open # before it spots left folding additional events # and updates the table. - TransactionDuration: 500ms # ZITADEL_PROJECTIONS_TRANSACTIONDURATION + TransactionDuration: 1m # ZITADEL_PROJECTIONS_TRANSACTIONDURATION # Time interval between scheduled projections RequeueEvery: 60s # ZITADEL_PROJECTIONS_REQUEUEEVERY # Time between retried database statements resulting from projected events @@ -246,10 +246,7 @@ Projections: HandleActiveInstances: 0s # ZITADEL_PROJECTIONS_HANDLEACTIVEINSTANCES # In the Customizations section, all settings from above can be overwritten for each specific projection Customizations: - Projects: - TransactionDuration: 2s custom_texts: - TransactionDuration: 2s BulkLimit: 400 project_grant_fields: TransactionDuration: 0s diff --git a/docs/docs/support/advisory/a10012.md b/docs/docs/support/advisory/a10012.md new file mode 100644 index 0000000000..fd08a9afd6 --- /dev/null +++ b/docs/docs/support/advisory/a10012.md @@ -0,0 +1,60 @@ +--- +title: Technical Advisory 10012 +--- + +## Date and Version + +Version: 2.63.0 + +Date: 2024-09-26 + +## Description + +In version 2.63.0 we've increased the transaction duration for projections. + +ZITADEL has an event driven architecture. After events are pushed to the eventstore, +they are reduced into projections in bulk batches. Projections allow for efficient lookup of data through normalized SQL tables. + +We've investigated multiple reports of outdated projections. +For example created users missing in get requests, or missing data after a ZITADEL upgrade[^1]. +The conclusion is that the transaction in which we perform a bulk of queries can timeout. +The old setting defined a transaction duration of 500ms for a bulk of 200 events. +A single event may create multiple statements in a single projection. +A timeout may occur even if the actual bulk size is less than 200, +which then results in more back-pressure on a busy system, leading to more timeouts and effectively dead-locking a projection. + +Increasing or disabling the projection transaction duration solved dead-locks in all reported cases. +We've decided to increase the transaction duration to 1 minute. +Due to the high value it is functionally similar to disabling, +however it still provides a safety net for transaction that do freeze, +perhaps due to connection issues with the database. + +[^1]: Changes written to the eventstore are the main source of truth. When a projection is out of date, some request may serve incomplete or no data. The data itself is however not lost. + +## Statement + +A summary of bug reports can be found in the following issue: [Missing data due to outdated projections](https://github.com/zitadel/zitadel/issues/8517). +This change was submitted in the following PR: +[fix(projection): increase transaction duration](https://github.com/zitadel/zitadel/pull/8632), which will be released in Version [2.63.0](https://github.com/zitadel/zitadel/releases/tag/v2.63.0) + +## Mitigation + +If you have a custom configuration for projections, this update will not apply to your system or some projections. When encountering projection dead-lock consider increasing the timeout to the new default value. + +Note that entries under `Customizations` overwrite the global settings for a single projection. + +```yaml +Projections: + TransactionDuration: 1m # ZITADEL_PROJECTIONS_TRANSACTIONDURATION + BulkLimit: 200 # ZITADEL_PROJECTIONS_BULKLIMIT + Customizations: + custom_texts: + BulkLimit: 400 + project_grant_fields: + TransactionDuration: 0s + BulkLimit: 2000 +``` + +## Impact + +Once this update has been released and deployed, transactions are allowed to run longer. No other functional impact is expected. diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml index 68e0b43f9c..6524b4806f 100644 --- a/internal/integration/config/zitadel.yaml +++ b/internal/integration/config/zitadel.yaml @@ -33,7 +33,6 @@ LogStore: Projections: HandleActiveInstances: 30m RequeueEvery: 5s - TransactionDuration: 1m Customizations: NotificationsQuotas: RequeueEvery: 1s From d01bd1c51aa41ead46edc6760e18782f8e656d87 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 17 Sep 2024 13:34:14 +0200 Subject: [PATCH 03/16] fix: correctly check app state on authentication (#8630) # Which Problems Are Solved In Zitadel, even after an organization is deactivated, associated projects, respectively their applications remain active. Users across other organizations can still log in and access through these applications, leading to unauthorized access. Additionally, if a project was deactivated access to applications was also still possible. # How the Problems Are Solved - Correctly check the status of the organization and related project. (Corresponding functions have been renamed to `Active...`) --- internal/api/oidc/client.go | 12 +- .../api/oidc/integration_test/client_test.go | 12 + internal/api/oidc/introspect.go | 2 +- internal/api/saml/storage.go | 10 +- internal/integration/oidc.go | 14 ++ internal/query/app.go | 217 ++++++++++-------- internal/query/app_test.go | 156 +++++++++++-- internal/query/introspection.go | 2 +- internal/query/introspection_client_by_id.sql | 5 +- internal/query/introspection_test.go | 4 +- internal/query/oidc_client.go | 2 +- internal/query/oidc_client_by_id.sql | 5 +- internal/query/oidc_client_test.go | 4 +- 13 files changed, 299 insertions(+), 146 deletions(-) diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 41fe19cb4a..6bca54c671 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -50,13 +50,10 @@ func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Cl err = oidcError(err) span.EndWithError(err) }() - client, err := o.query.GetOIDCClientByID(ctx, id, false) + client, err := o.query.ActiveOIDCClientByID(ctx, id, false) if err != nil { return nil, err } - if client.State != domain.AppStateActive { - return nil, zerrors.ThrowPreconditionFailed(nil, "OIDC-sdaGg", "client is not active") - } return ClientFromBusiness(client, o.defaultLoginURL, o.defaultLoginURLV2), nil } @@ -979,16 +976,13 @@ func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCreden if err != nil { return nil, err } - client, err := s.query.GetOIDCClientByID(ctx, clientID, assertion) + client, err := s.query.ActiveOIDCClientByID(ctx, clientID, assertion) if zerrors.IsNotFound(err) { - return nil, oidc.ErrInvalidClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("client not found") + return nil, oidc.ErrInvalidClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("no active client not found") } if err != nil { return nil, err // defaults to server error } - if client.State != domain.AppStateActive { - return nil, oidc.ErrInvalidClient().WithDescription("client is not active") - } if client.Settings == nil { client.Settings = &query.OIDCSettings{ AccessTokenLifetime: s.defaultAccessTokenLifetime, diff --git a/internal/api/oidc/integration_test/client_test.go b/internal/api/oidc/integration_test/client_test.go index 7e9f15ffac..1b9ccd5cb3 100644 --- a/internal/api/oidc/integration_test/client_test.go +++ b/internal/api/oidc/integration_test/client_test.go @@ -196,9 +196,13 @@ func TestServer_VerifyClient(t *testing.T) { sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) project, err := Instance.CreateProject(CTX) require.NoError(t, err) + projectInactive, err := Instance.CreateProject(CTX) + require.NoError(t, err) inactiveClient, err := Instance.CreateOIDCInactivateClient(CTX, redirectURI, logoutRedirectURI, project.GetId()) require.NoError(t, err) + inactiveProjectClient, err := Instance.CreateOIDCInactivateProjectClient(CTX, redirectURI, logoutRedirectURI, projectInactive.GetId()) + require.NoError(t, err) nativeClient, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) basicWebClient, err := Instance.CreateOIDCWebClientBasic(CTX, redirectURI, logoutRedirectURI, project.GetId()) @@ -240,6 +244,14 @@ func TestServer_VerifyClient(t *testing.T) { }, wantErr: true, }, + { + name: "client inactive (project) error", + client: clientDetails{ + authReqClientID: nativeClient.GetClientId(), + clientID: inactiveProjectClient.GetClientId(), + }, + wantErr: true, + }, { name: "native client success", client: clientDetails{ diff --git a/internal/api/oidc/introspect.go b/internal/api/oidc/introspect.go index 868676c1f1..c028013d6a 100644 --- a/internal/api/oidc/introspect.go +++ b/internal/api/oidc/introspect.go @@ -213,7 +213,7 @@ func (s *Server) clientFromCredentials(ctx context.Context, cc *op.ClientCredent if err != nil { return nil, err } - client, err = s.query.GetIntrospectionClientByID(ctx, clientID, assertion) + client, err = s.query.ActiveIntrospectionClientByID(ctx, clientID, assertion) if errors.Is(err, sql.ErrNoRows) { return nil, oidc.ErrUnauthorizedClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } diff --git a/internal/api/saml/storage.go b/internal/api/saml/storage.go index ca523398f7..8791619ba0 100644 --- a/internal/api/saml/storage.go +++ b/internal/api/saml/storage.go @@ -55,13 +55,10 @@ type Storage struct { } func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*serviceprovider.ServiceProvider, error) { - app, err := p.query.AppBySAMLEntityID(ctx, entityID) + app, err := p.query.ActiveAppBySAMLEntityID(ctx, entityID) if err != nil { return nil, err } - if app.State != domain.AppStateActive { - return nil, zerrors.ThrowPreconditionFailed(nil, "SAML-sdaGg", "app is not active") - } return serviceprovider.NewServiceProvider( app.ID, &serviceprovider.Config{ @@ -72,13 +69,10 @@ func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*servicep } func (p *Storage) GetEntityIDByAppID(ctx context.Context, appID string) (string, error) { - app, err := p.query.AppByID(ctx, appID) + app, err := p.query.AppByID(ctx, appID, true) if err != nil { return "", err } - if app.State != domain.AppStateActive { - return "", zerrors.ThrowPreconditionFailed(nil, "SAML-sdaGg", "app is not active") - } return app.SAMLConfig.EntityID, nil } diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index 9f394e2c2c..3afd262a35 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -107,6 +107,20 @@ func (i *Instance) CreateOIDCInactivateClient(ctx context.Context, redirectURI, return client, err } +func (i *Instance) CreateOIDCInactivateProjectClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) { + client, err := i.CreateOIDCNativeClient(ctx, redirectURI, logoutRedirectURI, projectID, false) + if err != nil { + return nil, err + } + _, err = i.Client.Mgmt.DeactivateProject(ctx, &management.DeactivateProjectRequest{ + Id: projectID, + }) + if err != nil { + return nil, err + } + return client, err +} + func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string) (*management.AddOIDCAppResponse, error) { project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), diff --git a/internal/query/app.go b/internal/query/app.go index 7d69981dd4..b94cb9cdaf 100644 --- a/internal/query/app.go +++ b/internal/query/app.go @@ -256,7 +256,7 @@ func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bo traceSpan.EndWithError(err) } - stmt, scan := prepareAppQuery(ctx, q.client) + stmt, scan := prepareAppQuery(ctx, q.client, false) eq := sq.Eq{ AppColumnID.identifier(): appID, AppColumnProjectID.identifier(): projectID, @@ -274,15 +274,20 @@ func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bo return app, err } -func (q *Queries) AppByID(ctx context.Context, appID string) (app *App, err error) { +func (q *Queries) AppByID(ctx context.Context, appID string, activeOnly bool) (app *App, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareAppQuery(ctx, q.client) + stmt, scan := prepareAppQuery(ctx, q.client, activeOnly) eq := sq.Eq{ AppColumnID.identifier(): appID, AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } + if activeOnly { + eq[AppColumnState.identifier()] = domain.AppStateActive + eq[ProjectColumnState.identifier()] = domain.ProjectStateActive + eq[OrgColumnState.identifier()] = domain.OrgStateActive + } query, args, err := stmt.Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-immt9", "Errors.Query.SQLStatement") @@ -295,7 +300,7 @@ func (q *Queries) AppByID(ctx context.Context, appID string) (app *App, err erro return app, err } -func (q *Queries) AppBySAMLEntityID(ctx context.Context, entityID string) (app *App, err error) { +func (q *Queries) ActiveAppBySAMLEntityID(ctx context.Context, entityID string) (app *App, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -303,6 +308,9 @@ func (q *Queries) AppBySAMLEntityID(ctx context.Context, entityID string) (app * eq := sq.Eq{ AppSAMLConfigColumnEntityID.identifier(): entityID, AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + AppColumnState.identifier(): domain.AppStateActive, + ProjectColumnState.identifier(): domain.ProjectStateActive, + OrgColumnState.identifier(): domain.OrgStateActive, } query, args, err := stmt.Where(eq).ToSql() if err != nil { @@ -413,8 +421,13 @@ func (q *Queries) AppByClientID(ctx context.Context, clientID string) (app *App, ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareAppQuery(ctx, q.client) - eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} + stmt, scan := prepareAppQuery(ctx, q.client, true) + eq := sq.Eq{ + AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + AppColumnState.identifier(): domain.AppStateActive, + ProjectColumnState.identifier(): domain.ProjectStateActive, + OrgColumnState.identifier(): domain.OrgStateActive, + } query, args, err := stmt.Where(sq.And{ eq, sq.Or{ @@ -491,107 +504,121 @@ func NewAppProjectIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(AppColumnProjectID, id, TextEquals) } -func prepareAppQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { - return sq.Select( - AppColumnID.identifier(), - AppColumnName.identifier(), - AppColumnProjectID.identifier(), - AppColumnCreationDate.identifier(), - AppColumnChangeDate.identifier(), - AppColumnResourceOwner.identifier(), - AppColumnState.identifier(), - AppColumnSequence.identifier(), +func prepareAppQuery(ctx context.Context, db prepareDatabase, activeOnly bool) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + query := sq.Select( + AppColumnID.identifier(), + AppColumnName.identifier(), + AppColumnProjectID.identifier(), + AppColumnCreationDate.identifier(), + AppColumnChangeDate.identifier(), + AppColumnResourceOwner.identifier(), + AppColumnState.identifier(), + AppColumnSequence.identifier(), - AppAPIConfigColumnAppID.identifier(), - AppAPIConfigColumnClientID.identifier(), - AppAPIConfigColumnAuthMethod.identifier(), + AppAPIConfigColumnAppID.identifier(), + AppAPIConfigColumnClientID.identifier(), + AppAPIConfigColumnAuthMethod.identifier(), - AppOIDCConfigColumnAppID.identifier(), - AppOIDCConfigColumnVersion.identifier(), - AppOIDCConfigColumnClientID.identifier(), - AppOIDCConfigColumnRedirectUris.identifier(), - AppOIDCConfigColumnResponseTypes.identifier(), - AppOIDCConfigColumnGrantTypes.identifier(), - AppOIDCConfigColumnApplicationType.identifier(), - AppOIDCConfigColumnAuthMethodType.identifier(), - AppOIDCConfigColumnPostLogoutRedirectUris.identifier(), - AppOIDCConfigColumnDevMode.identifier(), - AppOIDCConfigColumnAccessTokenType.identifier(), - AppOIDCConfigColumnAccessTokenRoleAssertion.identifier(), - AppOIDCConfigColumnIDTokenRoleAssertion.identifier(), - AppOIDCConfigColumnIDTokenUserinfoAssertion.identifier(), - AppOIDCConfigColumnClockSkew.identifier(), - AppOIDCConfigColumnAdditionalOrigins.identifier(), - AppOIDCConfigColumnSkipNativeAppSuccessPage.identifier(), + AppOIDCConfigColumnAppID.identifier(), + AppOIDCConfigColumnVersion.identifier(), + AppOIDCConfigColumnClientID.identifier(), + AppOIDCConfigColumnRedirectUris.identifier(), + AppOIDCConfigColumnResponseTypes.identifier(), + AppOIDCConfigColumnGrantTypes.identifier(), + AppOIDCConfigColumnApplicationType.identifier(), + AppOIDCConfigColumnAuthMethodType.identifier(), + AppOIDCConfigColumnPostLogoutRedirectUris.identifier(), + AppOIDCConfigColumnDevMode.identifier(), + AppOIDCConfigColumnAccessTokenType.identifier(), + AppOIDCConfigColumnAccessTokenRoleAssertion.identifier(), + AppOIDCConfigColumnIDTokenRoleAssertion.identifier(), + AppOIDCConfigColumnIDTokenUserinfoAssertion.identifier(), + AppOIDCConfigColumnClockSkew.identifier(), + AppOIDCConfigColumnAdditionalOrigins.identifier(), + AppOIDCConfigColumnSkipNativeAppSuccessPage.identifier(), - AppSAMLConfigColumnAppID.identifier(), - AppSAMLConfigColumnEntityID.identifier(), - AppSAMLConfigColumnMetadata.identifier(), - AppSAMLConfigColumnMetadataURL.identifier(), - ).From(appsTable.identifier()). + AppSAMLConfigColumnAppID.identifier(), + AppSAMLConfigColumnEntityID.identifier(), + AppSAMLConfigColumnMetadata.identifier(), + AppSAMLConfigColumnMetadataURL.identifier(), + ).From(appsTable.identifier()). + PlaceholderFormat(sq.Dollar) + + if activeOnly { + return query. + LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). + LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). + LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID)). + LeftJoin(join(ProjectColumnID, AppColumnProjectID)). + LeftJoin(join(OrgColumnID, AppColumnResourceOwner) + db.Timetravel(call.Took(ctx))), + scanApp + } + return query. LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). - LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))). - PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*App, error) { - app := new(App) + LeftJoin(join(AppSAMLConfigColumnAppID, AppColumnID) + db.Timetravel(call.Took(ctx))), + scanApp +} - var ( - apiConfig = sqlAPIConfig{} - oidcConfig = sqlOIDCConfig{} - samlConfig = sqlSAMLConfig{} - ) +func scanApp(row *sql.Row) (*App, error) { + app := new(App) - err := row.Scan( - &app.ID, - &app.Name, - &app.ProjectID, - &app.CreationDate, - &app.ChangeDate, - &app.ResourceOwner, - &app.State, - &app.Sequence, + var ( + apiConfig = sqlAPIConfig{} + oidcConfig = sqlOIDCConfig{} + samlConfig = sqlSAMLConfig{} + ) - &apiConfig.appID, - &apiConfig.clientID, - &apiConfig.authMethod, + err := row.Scan( + &app.ID, + &app.Name, + &app.ProjectID, + &app.CreationDate, + &app.ChangeDate, + &app.ResourceOwner, + &app.State, + &app.Sequence, - &oidcConfig.appID, - &oidcConfig.version, - &oidcConfig.clientID, - &oidcConfig.redirectUris, - &oidcConfig.responseTypes, - &oidcConfig.grantTypes, - &oidcConfig.applicationType, - &oidcConfig.authMethodType, - &oidcConfig.postLogoutRedirectUris, - &oidcConfig.devMode, - &oidcConfig.accessTokenType, - &oidcConfig.accessTokenRoleAssertion, - &oidcConfig.iDTokenRoleAssertion, - &oidcConfig.iDTokenUserinfoAssertion, - &oidcConfig.clockSkew, - &oidcConfig.additionalOrigins, - &oidcConfig.skipNativeAppSuccessPage, + &apiConfig.appID, + &apiConfig.clientID, + &apiConfig.authMethod, - &samlConfig.appID, - &samlConfig.entityID, - &samlConfig.metadata, - &samlConfig.metadataURL, - ) + &oidcConfig.appID, + &oidcConfig.version, + &oidcConfig.clientID, + &oidcConfig.redirectUris, + &oidcConfig.responseTypes, + &oidcConfig.grantTypes, + &oidcConfig.applicationType, + &oidcConfig.authMethodType, + &oidcConfig.postLogoutRedirectUris, + &oidcConfig.devMode, + &oidcConfig.accessTokenType, + &oidcConfig.accessTokenRoleAssertion, + &oidcConfig.iDTokenRoleAssertion, + &oidcConfig.iDTokenUserinfoAssertion, + &oidcConfig.clockSkew, + &oidcConfig.additionalOrigins, + &oidcConfig.skipNativeAppSuccessPage, - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, zerrors.ThrowNotFound(err, "QUERY-pCP8P", "Errors.App.NotExisting") - } - return nil, zerrors.ThrowInternal(err, "QUERY-4SJlx", "Errors.Internal") - } + &samlConfig.appID, + &samlConfig.entityID, + &samlConfig.metadata, + &samlConfig.metadataURL, + ) - apiConfig.set(app) - oidcConfig.set(app) - samlConfig.set(app) - - return app, nil + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-pCP8P", "Errors.App.NotExisting") } + return nil, zerrors.ThrowInternal(err, "QUERY-4SJlx", "Errors.Internal") + } + + apiConfig.set(app) + oidcConfig.set(app) + samlConfig.set(app) + + return app, nil } func prepareOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { @@ -690,6 +717,8 @@ func prepareSAMLAppQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil AppSAMLConfigColumnMetadataURL.identifier(), ).From(appsTable.identifier()). Join(join(AppSAMLConfigColumnAppID, AppColumnID)). + Join(join(ProjectColumnID, AppColumnProjectID)). + Join(join(OrgColumnID, AppColumnResourceOwner)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*App, error) { app := new(App) diff --git a/internal/query/app_test.go b/internal/query/app_test.go index 3ccec2e4c8..9a9c613868 100644 --- a/internal/query/app_test.go +++ b/internal/query/app_test.go @@ -1,6 +1,7 @@ package query import ( + "context" "database/sql" "database/sql/driver" "errors" @@ -9,13 +10,15 @@ import ( "testing" "time" + sq "github.com/Masterminds/squirrel" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" ) var ( - expectedAppQuery = regexp.QuoteMeta(`SELECT projections.apps7.id,` + + expectedAppQueryBase = `SELECT projections.apps7.id,` + ` projections.apps7.name,` + ` projections.apps7.project_id,` + ` projections.apps7.creation_date,` + @@ -53,8 +56,11 @@ var ( ` FROM projections.apps7` + ` LEFT JOIN projections.apps7_api_configs ON projections.apps7.id = projections.apps7_api_configs.app_id AND projections.apps7.instance_id = projections.apps7_api_configs.instance_id` + ` LEFT JOIN projections.apps7_oidc_configs ON projections.apps7.id = projections.apps7_oidc_configs.app_id AND projections.apps7.instance_id = projections.apps7_oidc_configs.instance_id` + - ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id` + - ` AS OF SYSTEM TIME '-1 ms'`) + ` LEFT JOIN projections.apps7_saml_configs ON projections.apps7.id = projections.apps7_saml_configs.app_id AND projections.apps7.instance_id = projections.apps7_saml_configs.instance_id` + expectedAppQuery = regexp.QuoteMeta(expectedAppQueryBase) + expectedActiveAppQuery = regexp.QuoteMeta(expectedAppQueryBase + + ` LEFT JOIN projections.projects4 ON projections.apps7.project_id = projections.projects4.id AND projections.apps7.instance_id = projections.projects4.instance_id` + + ` LEFT JOIN projections.orgs1 ON projections.apps7.resource_owner = projections.orgs1.id AND projections.apps7.instance_id = projections.orgs1.instance_id`) expectedAppsQuery = regexp.QuoteMeta(`SELECT projections.apps7.id,` + ` projections.apps7.name,` + ` projections.apps7.project_id,` + @@ -1140,8 +1146,10 @@ func Test_AppPrepare(t *testing.T) { object interface{} }{ { - name: "prepareAppQuery no result", - prepare: prepareAppQuery, + name: "prepareAppQuery no result", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(ctx, db, false) + }, want: want{ sqlExpectations: mockQueriesScanErr( expectedAppQuery, @@ -1158,8 +1166,10 @@ func Test_AppPrepare(t *testing.T) { object: (*App)(nil), }, { - name: "prepareAppQuery found", - prepare: prepareAppQuery, + name: "prepareAppQuery found", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(ctx, db, false) + }, want: want{ sqlExpectations: mockQuery( expectedAppQuery, @@ -1215,8 +1225,10 @@ func Test_AppPrepare(t *testing.T) { }, }, { - name: "prepareAppQuery api app", - prepare: prepareAppQuery, + name: "prepareAppQuery api app", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(ctx, db, false) + }, want: want{ sqlExpectations: mockQueries( expectedAppQuery, @@ -1278,8 +1290,10 @@ func Test_AppPrepare(t *testing.T) { }, }, { - name: "prepareAppQuery oidc app", - prepare: prepareAppQuery, + name: "prepareAppQuery oidc app", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(ctx, db, false) + }, want: want{ sqlExpectations: mockQueries( expectedAppQuery, @@ -1355,9 +1369,93 @@ func Test_AppPrepare(t *testing.T) { SkipNativeAppSuccessPage: false, }, }, - }, { - name: "prepareAppQuery saml app", - prepare: prepareAppQuery, + }, + { + name: "prepareAppQuery oidc app active only", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(ctx, db, true) + }, + want: want{ + sqlExpectations: mockQueries( + expectedActiveAppQuery, + appCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "app-id", + domain.OIDCVersionV1, + "oidc-client-id", + database.TextArray[string]{"https://redirect.to/me"}, + database.NumberArray[domain.OIDCResponseType]{domain.OIDCResponseTypeIDTokenToken}, + database.NumberArray[domain.OIDCGrantType]{domain.OIDCGrantTypeImplicit}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + database.TextArray[string]{"post.logout.ch"}, + true, + domain.OIDCTokenTypeJWT, + true, + true, + true, + 1 * time.Second, + database.TextArray[string]{"additional.origin"}, + false, + // saml config + nil, + nil, + nil, + nil, + }, + }, + ), + }, + object: &App{ + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: database.TextArray[string]{"https://redirect.to/me"}, + ResponseTypes: database.NumberArray[domain.OIDCResponseType]{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: database.NumberArray[domain.OIDCGrantType]{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: database.TextArray[string]{"post.logout.ch"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: true, + AssertIDTokenRole: true, + AssertIDTokenUserinfo: true, + ClockSkew: 1 * time.Second, + AdditionalOrigins: database.TextArray[string]{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: database.TextArray[string]{"https://redirect.to", "additional.origin"}, + SkipNativeAppSuccessPage: false, + }, + }, + }, + { + name: "prepareAppQuery saml app", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(ctx, db, false) + }, want: want{ sqlExpectations: mockQueries( expectedAppQuery, @@ -1420,8 +1518,10 @@ func Test_AppPrepare(t *testing.T) { }, }, { - name: "prepareAppQuery oidc app IsDevMode inactive", - prepare: prepareAppQuery, + name: "prepareAppQuery oidc app IsDevMode inactive", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(ctx, db, false) + }, want: want{ sqlExpectations: mockQueries( expectedAppQuery, @@ -1499,8 +1599,10 @@ func Test_AppPrepare(t *testing.T) { }, }, { - name: "prepareAppQuery oidc app AssertAccessTokenRole inactive", - prepare: prepareAppQuery, + name: "prepareAppQuery oidc app AssertAccessTokenRole inactive", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(ctx, db, false) + }, want: want{ sqlExpectations: mockQueries( expectedAppQuery, @@ -1578,8 +1680,10 @@ func Test_AppPrepare(t *testing.T) { }, }, { - name: "prepareAppQuery oidc app AssertIDTokenRole inactive", - prepare: prepareAppQuery, + name: "prepareAppQuery oidc app AssertIDTokenRole inactive", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(ctx, db, false) + }, want: want{ sqlExpectations: mockQueries( expectedAppQuery, @@ -1657,8 +1761,10 @@ func Test_AppPrepare(t *testing.T) { }, }, { - name: "prepareAppQuery oidc app AssertIDTokenUserinfo inactive", - prepare: prepareAppQuery, + name: "prepareAppQuery oidc app AssertIDTokenUserinfo inactive", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(ctx, db, false) + }, want: want{ sqlExpectations: mockQueries( expectedAppQuery, @@ -1736,8 +1842,10 @@ func Test_AppPrepare(t *testing.T) { }, }, { - name: "prepareAppQuery sql err", - prepare: prepareAppQuery, + name: "prepareAppQuery sql err", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return prepareAppQuery(ctx, db, false) + }, want: want{ sqlExpectations: mockQueryErr( expectedAppQuery, diff --git a/internal/query/introspection.go b/internal/query/introspection.go index a7fdaab718..ee96bf576b 100644 --- a/internal/query/introspection.go +++ b/internal/query/introspection.go @@ -52,7 +52,7 @@ type IntrospectionClient struct { //go:embed introspection_client_by_id.sql var introspectionClientByIDQuery string -func (q *Queries) GetIntrospectionClientByID(ctx context.Context, clientID string, getKeys bool) (_ *IntrospectionClient, err error) { +func (q *Queries) ActiveIntrospectionClientByID(ctx context.Context, clientID string, getKeys bool) (_ *IntrospectionClient, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/query/introspection_client_by_id.sql b/internal/query/introspection_client_by_id.sql index 5129d99a70..1cc6baf9ad 100644 --- a/internal/query/introspection_client_by_id.sql +++ b/internal/query/introspection_client_by_id.sql @@ -20,6 +20,7 @@ keys as ( ) select config.app_id, config.client_id, config.client_secret, config.app_type, apps.project_id, apps.resource_owner, p.project_role_assertion, keys.public_keys from config -join projections.apps7 apps on apps.id = config.app_id and apps.instance_id = config.instance_id -join projections.projects4 p on p.id = apps.project_id and p.instance_id = $1 +join projections.apps7 apps on apps.id = config.app_id and apps.instance_id = config.instance_id and apps.state = 1 +join projections.projects4 p on p.id = apps.project_id and p.instance_id = $1 and p.state = 1 +join projections.orgs1 o on o.id = p.resource_owner and o.instance_id = config.instance_id and o.org_state = 1 left join keys on keys.client_id = config.client_id; diff --git a/internal/query/introspection_test.go b/internal/query/introspection_test.go index 6535bd1639..4346842bf9 100644 --- a/internal/query/introspection_test.go +++ b/internal/query/introspection_test.go @@ -14,7 +14,7 @@ import ( "github.com/zitadel/zitadel/internal/database" ) -func TestQueries_GetIntrospectionClientByID(t *testing.T) { +func TestQueries_ActiveIntrospectionClientByID(t *testing.T) { pubkeys := database.Map[[]byte]{ "key1": {1, 2, 3}, "key2": {4, 5, 6}, @@ -96,7 +96,7 @@ func TestQueries_GetIntrospectionClientByID(t *testing.T) { }, } ctx := authz.NewMockContext("instanceID", "orgID", "userID") - got, err := q.GetIntrospectionClientByID(ctx, tt.args.clientID, tt.args.getKeys) + got, err := q.ActiveIntrospectionClientByID(ctx, tt.args.clientID, tt.args.getKeys) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) diff --git a/internal/query/oidc_client.go b/internal/query/oidc_client.go index d4364248bb..89d74d4ff8 100644 --- a/internal/query/oidc_client.go +++ b/internal/query/oidc_client.go @@ -45,7 +45,7 @@ type OIDCClient struct { //go:embed oidc_client_by_id.sql var oidcClientQuery string -func (q *Queries) GetOIDCClientByID(ctx context.Context, clientID string, getKeys bool) (client *OIDCClient, err error) { +func (q *Queries) ActiveOIDCClientByID(ctx context.Context, clientID string, getKeys bool) (client *OIDCClient, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/query/oidc_client_by_id.sql b/internal/query/oidc_client_by_id.sql index 3a0a0a0c95..ef471387b3 100644 --- a/internal/query/oidc_client_by_id.sql +++ b/internal/query/oidc_client_by_id.sql @@ -5,8 +5,9 @@ with client as ( c.access_token_type, c.access_token_role_assertion, c.id_token_role_assertion, c.id_token_userinfo_assertion, c.clock_skew, c.additional_origins, a.project_id, p.project_role_assertion from projections.apps7_oidc_configs c - join projections.apps7 a on a.id = c.app_id and a.instance_id = c.instance_id - join projections.projects4 p on p.id = a.project_id and p.instance_id = a.instance_id + join projections.apps7 a on a.id = c.app_id and a.instance_id = c.instance_id and a.state = 1 + join projections.projects4 p on p.id = a.project_id and p.instance_id = a.instance_id and p.state = 1 + join projections.orgs1 o on o.id = p.resource_owner and o.instance_id = c.instance_id and o.org_state = 1 where c.instance_id = $1 and c.client_id = $2 ), diff --git a/internal/query/oidc_client_test.go b/internal/query/oidc_client_test.go index 357c34f4e4..bb0890bff3 100644 --- a/internal/query/oidc_client_test.go +++ b/internal/query/oidc_client_test.go @@ -29,7 +29,7 @@ var ( testdataOidcClientNoSettings string ) -func TestQueries_GetOIDCClientByID(t *testing.T) { +func TestQueries_ActiveOIDCClientByID(t *testing.T) { expQuery := regexp.QuoteMeta(oidcClientQuery) cols := []string{"client"} pubkey := `-----BEGIN RSA PUBLIC KEY----- @@ -232,7 +232,7 @@ low2kyJov38V4Uk2I8kuXpLcnrpw5Tio2ooiUE27b0vHZqBKOei9Uo88qCrn3EKx }, } ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") - got, err := q.GetOIDCClientByID(ctx, "clientID", true) + got, err := q.ActiveOIDCClientByID(ctx, "clientID", true) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) From 4ac722d9341888ac3e66a30a475550f6a1600308 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:53:43 +0200 Subject: [PATCH 04/16] fix: use body for update user on user v2 API (#8635) Use body for update user endpoint on user v2 API. --- proto/zitadel/user/v2/user_service.proto | 1 + 1 file changed, 1 insertion(+) diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 9b82bfe297..20d3b65b67 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -385,6 +385,7 @@ service UserService { rpc UpdateHumanUser(UpdateHumanUserRequest) returns (UpdateHumanUserResponse) { option (google.api.http) = { put: "/v2/users/human/{user_id}" + body: "*" }; option (zitadel.protoc_gen_zitadel.v2.options) = { From ca1914e235df8eb62189cec07eb0de2cdad29629 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 17 Sep 2024 14:18:29 +0200 Subject: [PATCH 05/16] fix: user grants deactivation (#8634) # Which Problems Are Solved ZITADEL's user grants deactivation mechanism did not work correctly. Deactivated user grants were still provided in token, which could lead to unauthorized access to applications and resources. Additionally, the management and auth API always returned the state as active or did not provide any information about the state. # How the Problems Are Solved - Correctly check the user grant state on active for tokens and user information (userinfo, introspection, saml attributes) - Map state in API and display in Console --- .../user-grants/user-grants.component.html | 17 ++++++++++++++++- .../user-grants/user-grants.component.ts | 10 +++++++++- .../src/app/pages/grants/grants.component.html | 1 + .../granted-project-detail.component.html | 11 ++++++++++- .../owned-project-detail.component.html | 2 +- .../auth-user-detail.component.html | 11 ++++++++++- .../user-detail/user-detail.component.html | 2 +- internal/api/grpc/auth/user_grant.go | 1 + internal/api/grpc/user/user_grant.go | 17 ++++++++++++++++- internal/api/oidc/client.go | 13 +++++++++---- internal/api/saml/storage.go | 5 +++++ .../auth/repository/eventsourcing/repository.go | 7 ++++++- internal/query/user_grant.go | 4 ++++ internal/query/userinfo_by_id.sql | 1 + proto/zitadel/auth.proto | 5 +++++ 15 files changed, 95 insertions(+), 12 deletions(-) diff --git a/console/src/app/modules/user-grants/user-grants.component.html b/console/src/app/modules/user-grants/user-grants.component.html index efca9f74ec..82de67c8bb 100644 --- a/console/src/app/modules/user-grants/user-grants.component.html +++ b/console/src/app/modules/user-grants/user-grants.component.html @@ -154,10 +154,25 @@ + + {{ 'PROJECT.GRANT.STATE' | translate }} + + + {{ 'USER.DATA.STATE' + grant.state | translate }} + + + + - +