From 41ae35f2ef82c22738739532f84d8a1cdfa7b1b2 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:46:45 +0200 Subject: [PATCH] feat: add schema user create and remove (#8494) # Which Problems Are Solved Added functionality that user with a userschema can be created and removed. # How the Problems Are Solved Added logic and moved APIs so that everything is API v3 conform. # Additional Changes - move of user and userschema API to resources folder - changed testing and parameters - some renaming # Additional Context closes #7308 --------- Co-authored-by: Elio Bischof --- cmd/start/start.go | 8 +- docs/docusaurus.config.js | 4 +- .../api/grpc/resources/user/v3alpha/server.go | 51 + .../user/v3alpha/server_integration_test.go | 72 ++ .../api/grpc/resources/user/v3alpha/user.go | 66 + .../user/v3alpha/user_integration_test.go | 354 ++++++ .../resources/userschema/v3alpha/query.go | 251 ++++ .../v3alpha/query_integration_test.go | 302 +++++ .../resources/userschema/v3alpha/server.go | 65 + .../v3alpha/server_integration_test.go | 71 ++ .../userschema/v3alpha/userschema.go | 149 +++ .../v3alpha/userschema_integration_test.go | 811 ++++++++++++ .../api/grpc/user/schema/v3alpha/schema.go | 386 ------ .../schema/v3alpha/schema_integration_test.go | 1109 ----------------- .../api/grpc/user/schema/v3alpha/server.go | 51 - internal/command/user_schema.go | 86 +- internal/command/user_schema_model.go | 21 +- internal/command/user_schema_test.go | 47 +- internal/command/user_v3.go | 220 ++++ internal/command/user_v3_model.go | 174 +++ internal/command/user_v3_test.go | 1103 ++++++++++++++++ internal/domain/schema/permission.go | 20 +- internal/domain/schema/permission_test.go | 20 +- internal/domain/schema/schema.go | 6 +- internal/execution/execution_test.go | 2 - internal/integration/client.go | 171 ++- internal/query/projection/user_schema.go | 10 +- internal/query/projection/user_schema_test.go | 19 +- internal/query/user_schema.go | 9 +- internal/query/user_schema_test.go | 56 +- internal/repository/user/schema/schema.go | 11 + .../repository/user/schemauser/aggregate.go | 25 + internal/repository/user/schemauser/email.go | 202 +++ .../repository/user/schemauser/eventstore.go | 9 + internal/repository/user/schemauser/phone.go | 198 +++ internal/repository/user/schemauser/user.go | 144 +++ internal/static/i18n/bg.yaml | 25 + internal/static/i18n/cs.yaml | 25 + internal/static/i18n/de.yaml | 25 + internal/static/i18n/en.yaml | 27 + internal/static/i18n/es.yaml | 25 + internal/static/i18n/fr.yaml | 25 + internal/static/i18n/it.yaml | 25 + internal/static/i18n/ja.yaml | 25 + internal/static/i18n/mk.yaml | 25 + internal/static/i18n/nl.yaml | 25 + internal/static/i18n/pl.yaml | 25 + internal/static/i18n/pt.yaml | 25 + internal/static/i18n/ru.yaml | 25 + internal/static/i18n/sv.yaml | 25 + internal/static/i18n/zh.yaml | 25 + proto/zitadel/object/v3alpha/object.proto | 8 + .../action/v3alpha/action_service.proto | 1 - .../user/v3alpha/authenticator.proto | 76 +- .../user/v3alpha/communication.proto | 4 +- .../{ => resources}/user/v3alpha/query.proto | 85 +- .../zitadel/resources/user/v3alpha/user.proto | 112 ++ .../user/v3alpha/user_service.proto | 755 ++++++----- .../userschema}/v3alpha/user_schema.proto | 82 +- .../v3alpha/user_schema_service.proto | 144 ++- proto/zitadel/user/v3alpha/user.proto | 66 - 61 files changed, 5766 insertions(+), 2247 deletions(-) create mode 100644 internal/api/grpc/resources/user/v3alpha/server.go create mode 100644 internal/api/grpc/resources/user/v3alpha/server_integration_test.go create mode 100644 internal/api/grpc/resources/user/v3alpha/user.go create mode 100644 internal/api/grpc/resources/user/v3alpha/user_integration_test.go create mode 100644 internal/api/grpc/resources/userschema/v3alpha/query.go create mode 100644 internal/api/grpc/resources/userschema/v3alpha/query_integration_test.go create mode 100644 internal/api/grpc/resources/userschema/v3alpha/server.go create mode 100644 internal/api/grpc/resources/userschema/v3alpha/server_integration_test.go create mode 100644 internal/api/grpc/resources/userschema/v3alpha/userschema.go create mode 100644 internal/api/grpc/resources/userschema/v3alpha/userschema_integration_test.go delete mode 100644 internal/api/grpc/user/schema/v3alpha/schema.go delete mode 100644 internal/api/grpc/user/schema/v3alpha/schema_integration_test.go delete mode 100644 internal/api/grpc/user/schema/v3alpha/server.go create mode 100644 internal/command/user_v3.go create mode 100644 internal/command/user_v3_model.go create mode 100644 internal/command/user_v3_test.go create mode 100644 internal/repository/user/schemauser/aggregate.go create mode 100644 internal/repository/user/schemauser/email.go create mode 100644 internal/repository/user/schemauser/eventstore.go create mode 100644 internal/repository/user/schemauser/phone.go create mode 100644 internal/repository/user/schemauser/user.go rename proto/zitadel/{ => resources}/user/v3alpha/authenticator.proto (84%) rename proto/zitadel/{ => resources}/user/v3alpha/communication.proto (96%) rename proto/zitadel/{ => resources}/user/v3alpha/query.proto (69%) create mode 100644 proto/zitadel/resources/user/v3alpha/user.proto rename proto/zitadel/{ => resources}/user/v3alpha/user_service.proto (74%) rename proto/zitadel/{user/schema => resources/userschema}/v3alpha/user_schema.proto (68%) rename proto/zitadel/{user/schema => resources/userschema}/v3alpha/user_schema_service.proto (73%) delete mode 100644 proto/zitadel/user/v3alpha/user.proto diff --git a/cmd/start/start.go b/cmd/start/start.go index 0ecff76a9..47bc33ca4 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -45,13 +45,14 @@ import ( org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha" + user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha" + userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha" "github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3" session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta" settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2" settings_v2beta "github.com/zitadel/zitadel/internal/api/grpc/settings/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/system" - user_schema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/user/schema/v3alpha" user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" http_util "github.com/zitadel/zitadel/internal/api/http" @@ -444,7 +445,10 @@ func startAPIs( if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, user_schema_v3_alpha.CreateServer(commands, queries)); err != nil { + if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil { + return nil, err + } + if err := apis.RegisterService(ctx, user_v3_alpha.CreateServer(commands, keys.User)); err != nil { return nil, err } if err := apis.RegisterService(ctx, webkey.CreateServer(commands, queries)); err != nil { diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index a07f6e8c7..0318ad074 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -317,7 +317,7 @@ module.exports = { }, }, user_schema_v3: { - specPath: ".artifacts/openapi/zitadel/user/schema/v3alpha/user_schema_service.swagger.json", + specPath: ".artifacts/openapi/zitadel/resources/userschema/v3alpha/user_schema_service.swagger.json", outputDir: "docs/apis/resources/user_schema_service_v3", sidebarOptions: { groupPathsBy: "tag", @@ -325,7 +325,7 @@ module.exports = { }, }, user_v3: { - specPath: ".artifacts/openapi/zitadel/user/v3alpha/user_service.swagger.json", + specPath: ".artifacts/openapi/zitadel/resources/user/v3alpha/user_service.swagger.json", outputDir: "docs/apis/resources/user_service_v3", sidebarOptions: { groupPathsBy: "tag", diff --git a/internal/api/grpc/resources/user/v3alpha/server.go b/internal/api/grpc/resources/user/v3alpha/server.go new file mode 100644 index 000000000..e18f01745 --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/server.go @@ -0,0 +1,51 @@ +package user + +import ( + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" + user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" +) + +var _ user.ZITADELUsersServer = (*Server)(nil) + +type Server struct { + user.UnimplementedZITADELUsersServer + command *command.Commands + userCodeAlg crypto.EncryptionAlgorithm +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + userCodeAlg crypto.EncryptionAlgorithm, +) *Server { + return &Server{ + command: command, + userCodeAlg: userCodeAlg, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + user.RegisterZITADELUsersServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return user.ZITADELUsers_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return user.ZITADELUsers_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return user.ZITADELUsers_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return user.RegisterZITADELUsersHandler +} diff --git a/internal/api/grpc/resources/user/v3alpha/server_integration_test.go b/internal/api/grpc/resources/user/v3alpha/server_integration_test.go new file mode 100644 index 000000000..9043e7894 --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/server_integration_test.go @@ -0,0 +1,72 @@ +//go:build integration + +package user_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "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 + Tester *integration.Tester + Client user.ZITADELUsersClient +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, _, cancel := integration.Contexts(time.Hour) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + + IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser) + UserCTX = Tester.WithAuthorization(ctx, integration.Login) + Client = Tester.Client.UserV3Alpha + return m.Run() + }()) +} + +func ensureFeatureEnabled(t *testing.T, iamOwnerCTX context.Context) { + f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.UserSchema.GetEnabled() { + return + } + _, err = Tester.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{ + UserSchema: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration := time.Minute + if ctxDeadline, ok := iamOwnerCTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(ttt, err) + if f.UserSchema.GetEnabled() { + return + } + }, + retryDuration, + 100*time.Millisecond, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/resources/user/v3alpha/user.go b/internal/api/grpc/resources/user/v3alpha/user.go new file mode 100644 index 000000000..ede2f122a --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/user.go @@ -0,0 +1,66 @@ +package user + +import ( + "context" + + "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/internal/api/authz" + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/zerrors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" +) + +func (s *Server) CreateUser(ctx context.Context, req *user.CreateUserRequest) (_ *user.CreateUserResponse, err error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + schemauser, err := createUserRequestToCreateSchemaUser(ctx, req) + if err != nil { + return nil, err + } + + if err := s.command.CreateSchemaUser(ctx, schemauser, s.userCodeAlg); err != nil { + return nil, err + } + return &user.CreateUserResponse{ + Details: resource_object.DomainToDetailsPb(schemauser.Details, object.OwnerType_OWNER_TYPE_ORG, schemauser.ResourceOwner), + EmailCode: gu.Ptr(schemauser.ReturnCodeEmail), + PhoneCode: gu.Ptr(schemauser.ReturnCodePhone), + }, nil +} + +func createUserRequestToCreateSchemaUser(ctx context.Context, req *user.CreateUserRequest) (*command.CreateSchemaUser, error) { + data, err := req.GetUser().GetData().MarshalJSON() + if err != nil { + return nil, err + } + return &command.CreateSchemaUser{ + ResourceOwner: authz.GetCtxData(ctx).OrgID, + SchemaID: req.GetUser().GetSchemaId(), + ID: req.GetUser().GetUserId(), + Data: data, + }, nil +} + +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()) + if err != nil { + return nil, err + } + return &user.DeleteUserResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), + }, nil +} + +func checkUserSchemaEnabled(ctx context.Context) error { + if authz.GetInstance(ctx).Features().UserSchema { + return nil + } + return zerrors.ThrowPreconditionFailed(nil, "TODO", "Errors.UserSchema.NotEnabled") +} diff --git a/internal/api/grpc/resources/user/v3alpha/user_integration_test.go b/internal/api/grpc/resources/user/v3alpha/user_integration_test.go new file mode 100644 index 000000000..bad87d7e5 --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/user_integration_test.go @@ -0,0 +1,354 @@ +//go:build integration + +package user_test + +import ( + "context" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/require" + "github.com/zitadel/logging" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" + user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" +) + +func TestServer_CreateUser(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + schema := []byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`) + schemaResp := Tester.CreateUserSchema(IAMOwnerCTX, schema) + permissionSchema := []byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "urn:zitadel:schema:permission": { + "owner": "r", + "self": "r" + }, + "type": "string" + } + } + }`) + permissionSchemaResp := Tester.CreateUserSchema(IAMOwnerCTX, permissionSchema) + orgResp := Tester.CreateOrganization(IAMOwnerCTX, gofakeit.Name(), gofakeit.Email()) + + type res struct { + want *resource_object.Details + returnCodeEmail bool + returnCodePhone bool + } + tests := []struct { + name string + ctx context.Context + req *user.CreateUserRequest + res res + wantErr bool + }{ + { + name: "user create, no schemaID", + ctx: IAMOwnerCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{Data: unmarshalJSON("{\"name\": \"user\"}")}, + }, + wantErr: true, + }, + { + name: "user create, no context", + ctx: context.Background(), + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: schemaResp.GetDetails().GetId(), + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + wantErr: true, + }, + { + name: "user create, no permission", + ctx: UserCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: schemaResp.GetDetails().GetId(), + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + wantErr: true, + }, + { + name: "user create, invalid schema permission, owner", + ctx: IAMOwnerCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: permissionSchemaResp.GetDetails().GetId(), + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + wantErr: true, + }, + { + name: "user create, no user data", + ctx: IAMOwnerCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: schemaResp.GetDetails().GetId(), + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "user create, ok", + ctx: IAMOwnerCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: schemaResp.GetDetails().GetId(), + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, { + name: "user create, full contact, ok", + ctx: IAMOwnerCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: schemaResp.GetDetails().GetId(), + Data: unmarshalJSON("{\"name\": \"user\"}"), + 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) { + got, err := Tester.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_DeleteUser(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + schema := []byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`) + schemaResp := Tester.CreateUserSchema(IAMOwnerCTX, schema) + orgResp := Tester.CreateOrganization(IAMOwnerCTX, gofakeit.Name(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, req *user.DeleteUserRequest) error + req *user.DeleteUserRequest + want *resource_object.Details + wantErr bool + }{ + { + name: "user delete, no userID", + ctx: IAMOwnerCTX, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + UserId: "", + }, + wantErr: true, + }, + { + name: "user delete, not existing", + ctx: IAMOwnerCTX, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + UserId: "notexisting", + }, + wantErr: true, + }, + { + name: "user delete, no context", + ctx: context.Background(), + dep: func(ctx context.Context, req *user.DeleteUserRequest) error { + userResp := Tester.CreateSchemaUser(IAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.UserId = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user delete, no permission", + ctx: UserCTX, + dep: func(ctx context.Context, req *user.DeleteUserRequest) error { + userResp := Tester.CreateSchemaUser(IAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.UserId = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user delete, ok", + ctx: IAMOwnerCTX, + dep: func(ctx context.Context, req *user.DeleteUserRequest) error { + userResp := Tester.CreateSchemaUser(ctx, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.UserId = 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(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dep != nil { + err := tt.dep(tt.ctx, tt.req) + require.NoError(t, err) + } + got, err := Tester.Client.UserV3Alpha.DeleteUser(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want, got.Details) + }) + } +} + +func unmarshalJSON(data string) *structpb.Struct { + user := new(structpb.Struct) + err := user.UnmarshalJSON([]byte(data)) + if err != nil { + logging.OnError(err).Fatal("unmarshalling user json") + } + return user +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/query.go b/internal/api/grpc/resources/userschema/v3alpha/query.go new file mode 100644 index 000000000..aa25136e2 --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/query.go @@ -0,0 +1,251 @@ +package userschema + +import ( + "context" + + "google.golang.org/protobuf/types/known/structpb" + + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" +) + +func (s *Server) SearchUserSchemas(ctx context.Context, req *schema.SearchUserSchemasRequest) (*schema.SearchUserSchemasResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + queries, err := s.searchUserSchemaToModel(req) + if err != nil { + return nil, err + } + res, err := s.query.SearchUserSchema(ctx, queries) + if err != nil { + return nil, err + } + userSchemas, err := userSchemasToPb(res.UserSchemas) + if err != nil { + return nil, err + } + return &schema.SearchUserSchemasResponse{ + Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, res.SearchResponse), + Result: userSchemas, + }, nil +} + +func (s *Server) GetUserSchemaByID(ctx context.Context, req *schema.GetUserSchemaByIDRequest) (*schema.GetUserSchemaByIDResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + res, err := s.query.GetUserSchemaByID(ctx, req.GetId()) + if err != nil { + return nil, err + } + userSchema, err := userSchemaToPb(res) + if err != nil { + return nil, err + } + return &schema.GetUserSchemaByIDResponse{ + Schema: userSchema, + }, nil +} + +func (s *Server) searchUserSchemaToModel(req *schema.SearchUserSchemasRequest) (*query.UserSchemaSearchQueries, error) { + offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query) + if err != nil { + return nil, err + } + queries, err := userSchemaFiltersToQuery(req.Filters, 0) // start at level 0 + if err != nil { + return nil, err + } + return &query.UserSchemaSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: userSchemaFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func userSchemaFieldNameToSortingColumn(field *schema.FieldName) query.Column { + if field == nil { + return query.UserSchemaCreationDateCol + } + switch *field { + case schema.FieldName_FIELD_NAME_TYPE: + return query.UserSchemaTypeCol + case schema.FieldName_FIELD_NAME_STATE: + return query.UserSchemaStateCol + case schema.FieldName_FIELD_NAME_REVISION: + return query.UserSchemaRevisionCol + case schema.FieldName_FIELD_NAME_CHANGE_DATE: + return query.UserSchemaChangeDateCol + case schema.FieldName_FIELD_NAME_CREATION_DATE: + return query.UserSchemaCreationDateCol + case schema.FieldName_FIELD_NAME_UNSPECIFIED: + return query.UserSchemaIDCol + default: + return query.UserSchemaIDCol + } +} + +func userSchemasToPb(schemas []*query.UserSchema) (_ []*schema.UserSchema, err error) { + userSchemas := make([]*schema.UserSchema, len(schemas)) + for i, userSchema := range schemas { + userSchemas[i], err = userSchemaToPb(userSchema) + if err != nil { + return nil, err + } + } + return userSchemas, nil +} + +func userSchemaToPb(userSchema *query.UserSchema) (*schema.UserSchema, error) { + s := new(structpb.Struct) + if err := s.UnmarshalJSON(userSchema.Schema); err != nil { + return nil, err + } + return &schema.UserSchema{ + Details: resource_object.DomainToDetailsPb(&userSchema.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, userSchema.ResourceOwner), + Type: userSchema.Type, + State: userSchemaStateToPb(userSchema.State), + Revision: userSchema.Revision, + Schema: s, + PossibleAuthenticators: authenticatorTypesToPb(userSchema.PossibleAuthenticators), + }, nil +} + +func authenticatorTypesToPb(authenticators []domain.AuthenticatorType) []schema.AuthenticatorType { + authTypes := make([]schema.AuthenticatorType, len(authenticators)) + for i, authenticator := range authenticators { + authTypes[i] = authenticatorTypeToPb(authenticator) + } + return authTypes +} + +func authenticatorTypeToPb(authenticator domain.AuthenticatorType) schema.AuthenticatorType { + switch authenticator { + case domain.AuthenticatorTypeUsername: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME + case domain.AuthenticatorTypePassword: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD + case domain.AuthenticatorTypeWebAuthN: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN + case domain.AuthenticatorTypeTOTP: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP + case domain.AuthenticatorTypeOTPEmail: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL + case domain.AuthenticatorTypeOTPSMS: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS + case domain.AuthenticatorTypeAuthenticationKey: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY + case domain.AuthenticatorTypeIdentityProvider: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER + case domain.AuthenticatorTypeUnspecified: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED + default: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED + } +} + +func userSchemaStateToPb(state domain.UserSchemaState) schema.State { + switch state { + case domain.UserSchemaStateActive: + return schema.State_STATE_ACTIVE + case domain.UserSchemaStateInactive: + return schema.State_STATE_INACTIVE + case domain.UserSchemaStateUnspecified, + domain.UserSchemaStateDeleted: + return schema.State_STATE_UNSPECIFIED + default: + return schema.State_STATE_UNSPECIFIED + } +} + +func userSchemaStateToDomain(state schema.State) domain.UserSchemaState { + switch state { + case schema.State_STATE_ACTIVE: + return domain.UserSchemaStateActive + case schema.State_STATE_INACTIVE: + return domain.UserSchemaStateInactive + case schema.State_STATE_UNSPECIFIED: + return domain.UserSchemaStateUnspecified + default: + return domain.UserSchemaStateUnspecified + } +} + +func userSchemaFiltersToQuery(queries []*schema.SearchFilter, level uint8) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = userSchemaFilterToQuery(query, level) + if err != nil { + return nil, err + } + } + return q, nil +} + +func userSchemaFilterToQuery(query *schema.SearchFilter, level uint8) (query.SearchQuery, error) { + if level > 20 { + // can't go deeper than 20 levels of nesting. + return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-zsQ97", "Errors.Query.TooManyNestingLevels") + } + switch q := query.Filter.(type) { + case *schema.SearchFilter_StateFilter: + return stateQueryToQuery(q.StateFilter) + case *schema.SearchFilter_TypeFilter: + return typeQueryToQuery(q.TypeFilter) + case *schema.SearchFilter_IdFilter: + return idQueryToQuery(q.IdFilter) + case *schema.SearchFilter_OrFilter: + return orQueryToQuery(q.OrFilter, level) + case *schema.SearchFilter_AndFilter: + return andQueryToQuery(q.AndFilter, level) + case *schema.SearchFilter_NotFilter: + return notQueryToQuery(q.NotFilter, level) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-vR9nC", "List.Query.Invalid") + } +} + +func stateQueryToQuery(q *schema.StateFilter) (query.SearchQuery, error) { + return query.NewUserSchemaStateSearchQuery(userSchemaStateToDomain(q.GetState())) +} + +func typeQueryToQuery(q *schema.TypeFilter) (query.SearchQuery, error) { + return query.NewUserSchemaTypeSearchQuery(q.GetType(), resource_object.TextMethodPbToQuery(q.GetMethod())) +} + +func idQueryToQuery(q *schema.IDFilter) (query.SearchQuery, error) { + return query.NewUserSchemaIDSearchQuery(q.GetId(), resource_object.TextMethodPbToQuery(q.GetMethod())) +} + +func orQueryToQuery(q *schema.OrFilter, level uint8) (query.SearchQuery, error) { + mappedQueries, err := userSchemaFiltersToQuery(q.GetQueries(), level+1) + if err != nil { + return nil, err + } + return query.NewUserOrSearchQuery(mappedQueries) +} + +func andQueryToQuery(q *schema.AndFilter, level uint8) (query.SearchQuery, error) { + mappedQueries, err := userSchemaFiltersToQuery(q.GetQueries(), level+1) + if err != nil { + return nil, err + } + return query.NewUserAndSearchQuery(mappedQueries) +} + +func notQueryToQuery(q *schema.NotFilter, level uint8) (query.SearchQuery, error) { + mappedQuery, err := userSchemaFilterToQuery(q.GetFilter(), level+1) + if err != nil { + return nil, err + } + return query.NewUserNotSearchQuery(mappedQuery) +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/query_integration_test.go b/internal/api/grpc/resources/userschema/v3alpha/query_integration_test.go new file mode 100644 index 000000000..18996fb3f --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/query_integration_test.go @@ -0,0 +1,302 @@ +//go:build integration + +package userschema_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/zitadel/zitadel/internal/api/grpc" + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" + schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" +) + +func TestServer_ListUserSchemas(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + userSchema := new(structpb.Struct) + err := userSchema.UnmarshalJSON([]byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": {} + }`)) + require.NoError(t, err) + type args struct { + ctx context.Context + req *schema.SearchUserSchemasRequest + prepare func(request *schema.SearchUserSchemasRequest, resp *schema.SearchUserSchemasResponse) error + } + tests := []struct { + name string + args args + want *schema.SearchUserSchemasResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &schema.SearchUserSchemasRequest{}, + }, + wantErr: true, + }, + { + name: "not found, error", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.SearchUserSchemasRequest{ + Filters: []*schema.SearchFilter{ + { + Filter: &schema.SearchFilter_IdFilter{ + IdFilter: &schema.IDFilter{ + Id: "notexisting", + }, + }, + }, + }, + }, + }, + want: &schema.SearchUserSchemasResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + AppliedLimit: 100, + }, + Result: []*schema.UserSchema{}, + }, + }, + { + name: "single (id), ok", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.SearchUserSchemasRequest{}, + prepare: func(request *schema.SearchUserSchemasRequest, resp *schema.SearchUserSchemasResponse) error { + schemaType := fmt.Sprint(time.Now().UnixNano() + 1) + createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType) + request.Filters = []*schema.SearchFilter{ + { + Filter: &schema.SearchFilter_IdFilter{ + IdFilter: &schema.IDFilter{ + Id: createResp.GetDetails().GetId(), + Method: object.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + } + resp.Result[0].Type = schemaType + resp.Result[0].Details = createResp.GetDetails() + // as schema is freshly created, the changed date is the created date + resp.Result[0].Details.Created = resp.Result[0].Details.GetChanged() + resp.Details.Timestamp = resp.Result[0].Details.GetChanged() + return nil + }, + }, + want: &schema.SearchUserSchemasResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*schema.UserSchema{ + { + State: schema.State_STATE_ACTIVE, + Revision: 1, + Schema: userSchema, + PossibleAuthenticators: nil, + }, + }, + }, + }, + { + name: "multiple (type), ok", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.SearchUserSchemasRequest{}, + prepare: func(request *schema.SearchUserSchemasRequest, resp *schema.SearchUserSchemasResponse) error { + schemaType := fmt.Sprint(time.Now().UnixNano()) + schemaType1 := schemaType + "_1" + schemaType2 := schemaType + "_2" + createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType1) + createResp2 := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType2) + + request.SortingColumn = gu.Ptr(schema.FieldName_FIELD_NAME_TYPE) + request.Query = &object.SearchQuery{Desc: false} + request.Filters = []*schema.SearchFilter{ + { + Filter: &schema.SearchFilter_TypeFilter{ + TypeFilter: &schema.TypeFilter{ + Type: schemaType, + Method: object.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH, + }, + }, + }, + } + + resp.Result[0].Type = schemaType1 + resp.Result[0].Details = createResp.GetDetails() + resp.Result[1].Type = schemaType2 + resp.Result[1].Details = createResp2.GetDetails() + return nil + }, + }, + want: &schema.SearchUserSchemasResponse{ + Details: &object.ListDetails{ + TotalResult: 2, + AppliedLimit: 100, + }, + Result: []*schema.UserSchema{ + { + State: schema.State_STATE_ACTIVE, + Revision: 1, + Schema: userSchema, + PossibleAuthenticators: nil, + }, + { + State: schema.State_STATE_ACTIVE, + Revision: 1, + Schema: userSchema, + PossibleAuthenticators: nil, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.prepare != nil { + err := tt.args.prepare(tt.args.req, tt.want) + require.NoError(t, err) + } + + retryDuration := 20 * time.Second + if ctxDeadline, ok := IAMOwnerCTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.SearchUserSchemas(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, err) + return + } + assert.NoError(ttt, err) + + // always first check length, otherwise its failed anyway + assert.Len(ttt, got.Result, len(tt.want.Result)) + for i := range tt.want.Result { + want := tt.want.Result[i] + got := got.Result[i] + + integration.AssertResourceDetails(t, want.GetDetails(), got.GetDetails()) + want.Details = got.Details + grpc.AllFieldsEqual(t, want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + } + integration.AssertListDetails(t, tt.want, got) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected user schema result") + }) + } +} + +func TestServer_GetUserSchemaByID(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + userSchema := new(structpb.Struct) + err := userSchema.UnmarshalJSON([]byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": {} + }`)) + require.NoError(t, err) + type args struct { + ctx context.Context + req *schema.GetUserSchemaByIDRequest + prepare func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error + } + tests := []struct { + name string + args args + want *schema.GetUserSchemaByIDResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &schema.GetUserSchemaByIDRequest{}, + prepare: func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error { + schemaType := fmt.Sprint(time.Now().UnixNano() + 1) + createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType) + request.Id = createResp.GetDetails().GetId() + return nil + }, + }, + wantErr: true, + }, + { + name: "not existing, error", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.GetUserSchemaByIDRequest{ + Id: "notexisting", + }, + }, + wantErr: true, + }, + { + name: "get, ok", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.GetUserSchemaByIDRequest{}, + prepare: func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error { + schemaType := fmt.Sprint(time.Now().UnixNano() + 1) + createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType) + request.Id = createResp.GetDetails().GetId() + + resp.Schema.Type = schemaType + resp.Schema.Details = createResp.GetDetails() + return nil + }, + }, + want: &schema.GetUserSchemaByIDResponse{ + Schema: &schema.UserSchema{ + State: schema.State_STATE_ACTIVE, + Revision: 1, + Schema: userSchema, + PossibleAuthenticators: nil, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.prepare != nil { + err := tt.args.prepare(tt.args.req, tt.want) + require.NoError(t, err) + } + + retryDuration := 5 * time.Second + if ctxDeadline, ok := IAMOwnerCTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.GetUserSchemaByID(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, err) + return + } + assert.NoError(ttt, err) + + integration.AssertResourceDetails(t, tt.want.GetSchema().GetDetails(), got.GetSchema().GetDetails()) + tt.want.Schema.Details = got.GetSchema().GetDetails() + grpc.AllFieldsEqual(t, tt.want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected user schema result") + }) + } +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/server.go b/internal/api/grpc/resources/userschema/v3alpha/server.go new file mode 100644 index 000000000..1674a0838 --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/server.go @@ -0,0 +1,65 @@ +package userschema + +import ( + "context" + + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" +) + +var _ schema.ZITADELUserSchemasServer = (*Server)(nil) + +type Server struct { + schema.UnimplementedZITADELUserSchemasServer + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries +} + +type Config struct{} + +func CreateServer( + systemDefaults systemdefaults.SystemDefaults, + command *command.Commands, + query *query.Queries, +) *Server { + return &Server{ + systemDefaults: systemDefaults, + command: command, + query: query, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + schema.RegisterZITADELUserSchemasServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return schema.ZITADELUserSchemas_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return schema.ZITADELUserSchemas_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return schema.ZITADELUserSchemas_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return schema.RegisterZITADELUserSchemasHandler +} + +func checkUserSchemaEnabled(ctx context.Context) error { + if authz.GetInstance(ctx).Features().UserSchema { + return nil + } + return zerrors.ThrowPreconditionFailed(nil, "SCHEMA-SFjk3", "Errors.UserSchema.NotEnabled") +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/server_integration_test.go b/internal/api/grpc/resources/userschema/v3alpha/server_integration_test.go new file mode 100644 index 000000000..7dbdd04cb --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/server_integration_test.go @@ -0,0 +1,71 @@ +//go:build integration + +package userschema_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "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 + Tester *integration.Tester + Client schema.ZITADELUserSchemasClient +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, _, cancel := integration.Contexts(5 * time.Minute) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + + IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser) + Client = Tester.Client.UserSchemaV3 + + return m.Run() + }()) +} + +func ensureFeatureEnabled(t *testing.T, iamOwnerCTX context.Context) { + f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.UserSchema.GetEnabled() { + return + } + _, err = Tester.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{ + UserSchema: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration := time.Minute + if ctxDeadline, ok := iamOwnerCTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(ttt, err) + if f.UserSchema.GetEnabled() { + return + } + }, + retryDuration, + 100*time.Millisecond, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/userschema.go b/internal/api/grpc/resources/userschema/v3alpha/userschema.go new file mode 100644 index 000000000..406b358b1 --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/userschema.go @@ -0,0 +1,149 @@ +package userschema + +import ( + "context" + + "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" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" +) + +func (s *Server) CreateUserSchema(ctx context.Context, req *schema.CreateUserSchemaRequest) (*schema.CreateUserSchemaResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + instanceID := authz.GetInstance(ctx).InstanceID() + userSchema, err := createUserSchemaToCommand(req, instanceID) + if err != nil { + return nil, err + } + + if err := s.command.CreateUserSchema(ctx, userSchema); err != nil { + return nil, err + } + return &schema.CreateUserSchemaResponse{ + Details: resource_object.DomainToDetailsPb(userSchema.Details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + }, nil +} + +func (s *Server) PatchUserSchema(ctx context.Context, req *schema.PatchUserSchemaRequest) (*schema.PatchUserSchemaResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + instanceID := authz.GetInstance(ctx).InstanceID() + userSchema, err := patchUserSchemaToCommand(req, instanceID) + if err != nil { + return nil, err + } + if err := s.command.ChangeUserSchema(ctx, userSchema); err != nil { + return nil, err + } + return &schema.PatchUserSchemaResponse{ + Details: resource_object.DomainToDetailsPb(userSchema.Details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + }, nil +} + +func (s *Server) DeactivateUserSchema(ctx context.Context, req *schema.DeactivateUserSchemaRequest) (*schema.DeactivateUserSchemaResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + instanceID := authz.GetInstance(ctx).InstanceID() + details, err := s.command.DeactivateUserSchema(ctx, req.GetId(), instanceID) + if err != nil { + return nil, err + } + return &schema.DeactivateUserSchemaResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + }, nil +} + +func (s *Server) ReactivateUserSchema(ctx context.Context, req *schema.ReactivateUserSchemaRequest) (*schema.ReactivateUserSchemaResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + instanceID := authz.GetInstance(ctx).InstanceID() + details, err := s.command.ReactivateUserSchema(ctx, req.GetId(), instanceID) + if err != nil { + return nil, err + } + return &schema.ReactivateUserSchemaResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + }, nil +} + +func (s *Server) DeleteUserSchema(ctx context.Context, req *schema.DeleteUserSchemaRequest) (*schema.DeleteUserSchemaResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + instanceID := authz.GetInstance(ctx).InstanceID() + details, err := s.command.DeleteUserSchema(ctx, req.GetId(), instanceID) + if err != nil { + return nil, err + } + return &schema.DeleteUserSchemaResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + }, nil +} + +func createUserSchemaToCommand(req *schema.CreateUserSchemaRequest, resourceOwner string) (*command.CreateUserSchema, error) { + schema, err := req.GetUserSchema().GetSchema().MarshalJSON() + if err != nil { + return nil, err + } + return &command.CreateUserSchema{ + ResourceOwner: resourceOwner, + Type: req.GetUserSchema().GetType(), + Schema: schema, + PossibleAuthenticators: authenticatorsToDomain(req.GetUserSchema().GetPossibleAuthenticators()), + }, nil +} + +func patchUserSchemaToCommand(req *schema.PatchUserSchemaRequest, resourceOwner string) (*command.ChangeUserSchema, error) { + schema, err := req.GetSchema().MarshalJSON() + if err != nil { + return nil, err + } + return &command.ChangeUserSchema{ + ID: req.GetId(), + ResourceOwner: resourceOwner, + Type: req.Type, + Schema: schema, + PossibleAuthenticators: authenticatorsToDomain(req.GetPossibleAuthenticators()), + }, nil +} + +func authenticatorsToDomain(authenticators []schema.AuthenticatorType) []domain.AuthenticatorType { + types := make([]domain.AuthenticatorType, len(authenticators)) + for i, authenticator := range authenticators { + types[i] = authenticatorTypeToDomain(authenticator) + } + return types +} + +func authenticatorTypeToDomain(authenticator schema.AuthenticatorType) domain.AuthenticatorType { + switch authenticator { + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED: + return domain.AuthenticatorTypeUnspecified + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME: + return domain.AuthenticatorTypeUsername + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD: + return domain.AuthenticatorTypePassword + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN: + return domain.AuthenticatorTypeWebAuthN + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP: + return domain.AuthenticatorTypeTOTP + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL: + return domain.AuthenticatorTypeOTPEmail + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS: + return domain.AuthenticatorTypeOTPSMS + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY: + return domain.AuthenticatorTypeAuthenticationKey + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER: + return domain.AuthenticatorTypeIdentityProvider + default: + return domain.AuthenticatorTypeUnspecified + } +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/userschema_integration_test.go b/internal/api/grpc/resources/userschema/v3alpha/userschema_integration_test.go new file mode 100644 index 000000000..8036c7533 --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/userschema_integration_test.go @@ -0,0 +1,811 @@ +//go:build integration + +package userschema_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" + schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" +) + +func TestServer_CreateUserSchema(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + tests := []struct { + name string + ctx context.Context + req *schema.CreateUserSchemaRequest + want *schema.CreateUserSchemaResponse + wantErr bool + }{ + { + name: "missing permission, error", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.CreateUserSchema{ + Type: gofakeit.Name(), + }, + }, + wantErr: true, + }, + { + name: "empty type", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.CreateUserSchema{ + Type: "", + }, + }, + wantErr: true, + }, + { + name: "empty schema, error", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.CreateUserSchema{ + Type: gofakeit.Name(), + }, + }, + wantErr: true, + }, + { + name: "invalid schema, error", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.CreateUserSchema{ + Type: gofakeit.Name(), + DataType: &schema.CreateUserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string", + "required": true + }, + "description": { + "type": "string" + } + } + } + `)) + require.NoError(t, err) + return s + }(), + }, + }, + }, + wantErr: true, + }, + { + name: "no authenticators, ok", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.CreateUserSchema{ + Type: gofakeit.Name(), + DataType: &schema.CreateUserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + }, + }, + want: &schema.CreateUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "invalid authenticator, error", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.CreateUserSchema{ + Type: gofakeit.Name(), + DataType: &schema.CreateUserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED, + }, + }, + }, + wantErr: true, + }, + { + name: "with authenticator, ok", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.CreateUserSchema{ + Type: gofakeit.Name(), + DataType: &schema.CreateUserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, + }, + }, + }, + want: &schema.CreateUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "with invalid permission, error", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.CreateUserSchema{ + Type: gofakeit.Name(), + DataType: &schema.CreateUserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string", + "urn:zitadel:schema:permission": "read" + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, + }, + }, + }, + wantErr: true, + }, + { + name: "with valid permission, ok", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.CreateUserSchema{ + Type: gofakeit.Name(), + DataType: &schema.CreateUserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string", + "urn:zitadel:schema:permission": { + "owner": "rw", + "self": "r" + } + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, + }, + }, + }, + want: &schema.CreateUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.CreateUserSchema(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails()) + }) + } +} + +func TestServer_UpdateUserSchema(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + type args struct { + ctx context.Context + req *schema.PatchUserSchemaRequest + } + tests := []struct { + name string + prepare func(request *schema.PatchUserSchemaRequest) error + args args + want *schema.PatchUserSchemaResponse + wantErr bool + }{ + { + name: "missing permission, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &schema.PatchUserSchemaRequest{ + Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), + }, + }, + wantErr: true, + }, + { + name: "missing id, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{}, + }, + wantErr: true, + }, + { + name: "not existing, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + request.Id = "notexisting" + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{}, + }, + wantErr: true, + }, + { + name: "empty type, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + Type: gu.Ptr(""), + }, + }, + wantErr: true, + }, + { + name: "update type, ok", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), + }, + }, + want: &schema.PatchUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "empty schema, ok", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + DataType: &schema.PatchUserSchemaRequest_Schema{}, + }, + }, + want: &schema.PatchUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "invalid schema, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + DataType: &schema.PatchUserSchemaRequest_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string", + "required": true + }, + "description": { + "type": "string" + } + } + } + `)) + require.NoError(t, err) + return s + }(), + }, + }, + }, + wantErr: true, + }, + { + name: "update schema, ok", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + DataType: &schema.PatchUserSchemaRequest_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + }, + }, + want: &schema.PatchUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "invalid authenticator, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED, + }, + }, + }, + wantErr: true, + }, + { + name: "update authenticator, ok", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, + }, + }, + }, + want: &schema.PatchUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "inactive, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + _, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ + Id: schemaID, + }) + require.NoError(t, err) + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.prepare(tt.args.req) + require.NoError(t, err) + + got, err := Client.PatchUserSchema(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails()) + }) + } +} + +func TestServer_DeactivateUserSchema(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + type args struct { + ctx context.Context + req *schema.DeactivateUserSchemaRequest + prepare func(request *schema.DeactivateUserSchemaRequest) error + } + tests := []struct { + name string + args args + want *schema.DeactivateUserSchemaResponse + wantErr bool + }{ + { + name: "not existing, error", + args: args{ + IAMOwnerCTX, + &schema.DeactivateUserSchemaRequest{ + Id: "notexisting", + }, + func(request *schema.DeactivateUserSchemaRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "active, ok", + args: args{ + IAMOwnerCTX, + &schema.DeactivateUserSchemaRequest{}, + func(request *schema.DeactivateUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + }, + want: &schema.DeactivateUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "inactive, error", + args: args{ + IAMOwnerCTX, + &schema.DeactivateUserSchemaRequest{}, + func(request *schema.DeactivateUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + _, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ + Id: schemaID, + }) + return err + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + + got, err := Client.DeactivateUserSchema(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails()) + }) + } +} + +func TestServer_ReactivateUserSchema(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + type args struct { + ctx context.Context + req *schema.ReactivateUserSchemaRequest + prepare func(request *schema.ReactivateUserSchemaRequest) error + } + tests := []struct { + name string + args args + want *schema.ReactivateUserSchemaResponse + wantErr bool + }{ + { + name: "not existing, error", + args: args{ + IAMOwnerCTX, + &schema.ReactivateUserSchemaRequest{ + Id: "notexisting", + }, + func(request *schema.ReactivateUserSchemaRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "active, error", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.ReactivateUserSchemaRequest{}, + prepare: func(request *schema.ReactivateUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + }, + wantErr: true, + }, + { + name: "inactive, ok", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.ReactivateUserSchemaRequest{}, + prepare: func(request *schema.ReactivateUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + _, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ + Id: schemaID, + }) + return err + }, + }, + want: &schema.ReactivateUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + + got, err := Client.ReactivateUserSchema(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails()) + }) + } +} + +func TestServer_DeleteUserSchema(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + type args struct { + ctx context.Context + req *schema.DeleteUserSchemaRequest + prepare func(request *schema.DeleteUserSchemaRequest) error + } + tests := []struct { + name string + args args + want *schema.DeleteUserSchemaResponse + wantErr bool + }{ + { + name: "not existing, error", + args: args{ + IAMOwnerCTX, + &schema.DeleteUserSchemaRequest{ + Id: "notexisting", + }, + func(request *schema.DeleteUserSchemaRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "delete, ok", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.DeleteUserSchemaRequest{}, + prepare: func(request *schema.DeleteUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + }, + want: &schema.DeleteUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "deleted, error", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.DeleteUserSchemaRequest{}, + prepare: func(request *schema.DeleteUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + _, err := Client.DeleteUserSchema(IAMOwnerCTX, &schema.DeleteUserSchemaRequest{ + Id: schemaID, + }) + return err + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + + got, err := Client.DeleteUserSchema(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails()) + }) + } +} diff --git a/internal/api/grpc/user/schema/v3alpha/schema.go b/internal/api/grpc/user/schema/v3alpha/schema.go deleted file mode 100644 index 73342f702..000000000 --- a/internal/api/grpc/user/schema/v3alpha/schema.go +++ /dev/null @@ -1,386 +0,0 @@ -package schema - -import ( - "context" - - "google.golang.org/protobuf/types/known/structpb" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" - schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha" -) - -func (s *Server) CreateUserSchema(ctx context.Context, req *schema.CreateUserSchemaRequest) (*schema.CreateUserSchemaResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - userSchema, err := createUserSchemaToCommand(req, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - id, details, err := s.command.CreateUserSchema(ctx, userSchema) - if err != nil { - return nil, err - } - return &schema.CreateUserSchemaResponse{ - Id: id, - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) UpdateUserSchema(ctx context.Context, req *schema.UpdateUserSchemaRequest) (*schema.UpdateUserSchemaResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - userSchema, err := updateUserSchemaToCommand(req, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - details, err := s.command.UpdateUserSchema(ctx, userSchema) - if err != nil { - return nil, err - } - return &schema.UpdateUserSchemaResponse{ - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) DeactivateUserSchema(ctx context.Context, req *schema.DeactivateUserSchemaRequest) (*schema.DeactivateUserSchemaResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - details, err := s.command.DeactivateUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - return &schema.DeactivateUserSchemaResponse{ - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) ReactivateUserSchema(ctx context.Context, req *schema.ReactivateUserSchemaRequest) (*schema.ReactivateUserSchemaResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - details, err := s.command.ReactivateUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - return &schema.ReactivateUserSchemaResponse{ - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) DeleteUserSchema(ctx context.Context, req *schema.DeleteUserSchemaRequest) (*schema.DeleteUserSchemaResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - details, err := s.command.DeleteUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - return &schema.DeleteUserSchemaResponse{ - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) ListUserSchemas(ctx context.Context, req *schema.ListUserSchemasRequest) (*schema.ListUserSchemasResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - queries, err := listUserSchemaToQuery(req) - if err != nil { - return nil, err - } - res, err := s.query.SearchUserSchema(ctx, queries) - if err != nil { - return nil, err - } - userSchemas, err := userSchemasToPb(res.UserSchemas) - if err != nil { - return nil, err - } - return &schema.ListUserSchemasResponse{ - Details: object.ToListDetails(res.SearchResponse), - Result: userSchemas, - }, nil -} - -func (s *Server) GetUserSchemaByID(ctx context.Context, req *schema.GetUserSchemaByIDRequest) (*schema.GetUserSchemaByIDResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - res, err := s.query.GetUserSchemaByID(ctx, req.GetId()) - if err != nil { - return nil, err - } - userSchema, err := userSchemaToPb(res) - if err != nil { - return nil, err - } - return &schema.GetUserSchemaByIDResponse{ - Schema: userSchema, - }, nil -} - -func userSchemasToPb(schemas []*query.UserSchema) (_ []*schema.UserSchema, err error) { - userSchemas := make([]*schema.UserSchema, len(schemas)) - for i, userSchema := range schemas { - userSchemas[i], err = userSchemaToPb(userSchema) - if err != nil { - return nil, err - } - } - return userSchemas, nil -} - -func userSchemaToPb(userSchema *query.UserSchema) (*schema.UserSchema, error) { - s := new(structpb.Struct) - if err := s.UnmarshalJSON(userSchema.Schema); err != nil { - return nil, err - } - return &schema.UserSchema{ - Id: userSchema.ID, - Details: object.DomainToDetailsPb(&userSchema.ObjectDetails), - Type: userSchema.Type, - State: userSchemaStateToPb(userSchema.State), - Revision: userSchema.Revision, - Schema: s, - PossibleAuthenticators: authenticatorTypesToPb(userSchema.PossibleAuthenticators), - }, nil -} - -func authenticatorTypesToPb(authenticators []domain.AuthenticatorType) []schema.AuthenticatorType { - authTypes := make([]schema.AuthenticatorType, len(authenticators)) - for i, authenticator := range authenticators { - authTypes[i] = authenticatorTypeToPb(authenticator) - } - return authTypes -} - -func authenticatorTypeToPb(authenticator domain.AuthenticatorType) schema.AuthenticatorType { - switch authenticator { - case domain.AuthenticatorTypeUsername: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME - case domain.AuthenticatorTypePassword: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD - case domain.AuthenticatorTypeWebAuthN: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN - case domain.AuthenticatorTypeTOTP: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP - case domain.AuthenticatorTypeOTPEmail: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL - case domain.AuthenticatorTypeOTPSMS: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS - case domain.AuthenticatorTypeAuthenticationKey: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY - case domain.AuthenticatorTypeIdentityProvider: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER - case domain.AuthenticatorTypeUnspecified: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED - default: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED - } -} - -func userSchemaStateToPb(state domain.UserSchemaState) schema.State { - switch state { - case domain.UserSchemaStateActive: - return schema.State_STATE_ACTIVE - case domain.UserSchemaStateInactive: - return schema.State_STATE_INACTIVE - case domain.UserSchemaStateUnspecified, - domain.UserSchemaStateDeleted: - return schema.State_STATE_UNSPECIFIED - default: - return schema.State_STATE_UNSPECIFIED - } -} - -func listUserSchemaToQuery(req *schema.ListUserSchemasRequest) (*query.UserSchemaSearchQueries, error) { - offset, limit, asc := object.ListQueryToQuery(req.Query) - queries, err := userSchemaQueriesToQuery(req.Queries, 0) // start at level 0 - if err != nil { - return nil, err - } - return &query.UserSchemaSearchQueries{ - SearchRequest: query.SearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - SortingColumn: userSchemaFieldNameToSortingColumn(req.SortingColumn), - }, - Queries: queries, - }, nil -} - -func userSchemaFieldNameToSortingColumn(column schema.FieldName) query.Column { - switch column { - case schema.FieldName_FIELD_NAME_TYPE: - return query.UserSchemaTypeCol - case schema.FieldName_FIELD_NAME_STATE: - return query.UserSchemaStateCol - case schema.FieldName_FIELD_NAME_REVISION: - return query.UserSchemaRevisionCol - case schema.FieldName_FIELD_NAME_CHANGE_DATE: - return query.UserSchemaChangeDateCol - case schema.FieldName_FIELD_NAME_UNSPECIFIED: - return query.UserSchemaIDCol - default: - return query.UserSchemaIDCol - } -} - -func checkUserSchemaEnabled(ctx context.Context) error { - if authz.GetInstance(ctx).Features().UserSchema { - return nil - } - return zerrors.ThrowPreconditionFailed(nil, "SCHEMA-SFjk3", "Errors.UserSchema.NotEnabled") -} - -func createUserSchemaToCommand(req *schema.CreateUserSchemaRequest, resourceOwner string) (*command.CreateUserSchema, error) { - schema, err := req.GetSchema().MarshalJSON() - if err != nil { - return nil, err - } - return &command.CreateUserSchema{ - ResourceOwner: resourceOwner, - Type: req.GetType(), - Schema: schema, - PossibleAuthenticators: authenticatorsToDomain(req.GetPossibleAuthenticators()), - }, nil -} - -func updateUserSchemaToCommand(req *schema.UpdateUserSchemaRequest, resourceOwner string) (*command.UpdateUserSchema, error) { - schema, err := req.GetSchema().MarshalJSON() - if err != nil { - return nil, err - } - return &command.UpdateUserSchema{ - ID: req.GetId(), - ResourceOwner: resourceOwner, - Type: req.Type, - Schema: schema, - PossibleAuthenticators: authenticatorsToDomain(req.GetPossibleAuthenticators()), - }, nil -} - -func authenticatorsToDomain(authenticators []schema.AuthenticatorType) []domain.AuthenticatorType { - types := make([]domain.AuthenticatorType, len(authenticators)) - for i, authenticator := range authenticators { - types[i] = authenticatorTypeToDomain(authenticator) - } - return types -} - -func authenticatorTypeToDomain(authenticator schema.AuthenticatorType) domain.AuthenticatorType { - switch authenticator { - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED: - return domain.AuthenticatorTypeUnspecified - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME: - return domain.AuthenticatorTypeUsername - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD: - return domain.AuthenticatorTypePassword - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN: - return domain.AuthenticatorTypeWebAuthN - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP: - return domain.AuthenticatorTypeTOTP - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL: - return domain.AuthenticatorTypeOTPEmail - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS: - return domain.AuthenticatorTypeOTPSMS - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY: - return domain.AuthenticatorTypeAuthenticationKey - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER: - return domain.AuthenticatorTypeIdentityProvider - default: - return domain.AuthenticatorTypeUnspecified - } -} - -func userSchemaStateToDomain(state schema.State) domain.UserSchemaState { - switch state { - case schema.State_STATE_ACTIVE: - return domain.UserSchemaStateActive - case schema.State_STATE_INACTIVE: - return domain.UserSchemaStateInactive - case schema.State_STATE_UNSPECIFIED: - return domain.UserSchemaStateUnspecified - default: - return domain.UserSchemaStateUnspecified - } -} - -func userSchemaQueriesToQuery(queries []*schema.SearchQuery, level uint8) (_ []query.SearchQuery, err error) { - q := make([]query.SearchQuery, len(queries)) - for i, query := range queries { - q[i], err = userSchemaQueryToQuery(query, level) - if err != nil { - return nil, err - } - } - return q, nil -} - -func userSchemaQueryToQuery(query *schema.SearchQuery, level uint8) (query.SearchQuery, error) { - if level > 20 { - // can't go deeper than 20 levels of nesting. - return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-zsQ97", "Errors.Query.TooManyNestingLevels") - } - switch q := query.Query.(type) { - case *schema.SearchQuery_StateQuery: - return stateQueryToQuery(q.StateQuery) - case *schema.SearchQuery_TypeQuery: - return typeQueryToQuery(q.TypeQuery) - case *schema.SearchQuery_IdQuery: - return idQueryToQuery(q.IdQuery) - case *schema.SearchQuery_OrQuery: - return orQueryToQuery(q.OrQuery, level) - case *schema.SearchQuery_AndQuery: - return andQueryToQuery(q.AndQuery, level) - case *schema.SearchQuery_NotQuery: - return notQueryToQuery(q.NotQuery, level) - default: - return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-vR9nC", "List.Query.Invalid") - } -} - -func stateQueryToQuery(q *schema.StateQuery) (query.SearchQuery, error) { - return query.NewUserSchemaStateSearchQuery(userSchemaStateToDomain(q.GetState())) -} - -func typeQueryToQuery(q *schema.TypeQuery) (query.SearchQuery, error) { - return query.NewUserSchemaTypeSearchQuery(q.GetType(), object.TextMethodToQuery(q.GetMethod())) -} - -func idQueryToQuery(q *schema.IDQuery) (query.SearchQuery, error) { - return query.NewUserSchemaIDSearchQuery(q.GetId(), object.TextMethodToQuery(q.GetMethod())) -} - -func orQueryToQuery(q *schema.OrQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, err := userSchemaQueriesToQuery(q.GetQueries(), level+1) - if err != nil { - return nil, err - } - return query.NewUserOrSearchQuery(mappedQueries) -} - -func andQueryToQuery(q *schema.AndQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, err := userSchemaQueriesToQuery(q.GetQueries(), level+1) - if err != nil { - return nil, err - } - return query.NewUserAndSearchQuery(mappedQueries) -} - -func notQueryToQuery(q *schema.NotQuery, level uint8) (query.SearchQuery, error) { - mappedQuery, err := userSchemaQueryToQuery(q.GetQuery(), level+1) - if err != nil { - return nil, err - } - return query.NewUserNotSearchQuery(mappedQuery) -} diff --git a/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go b/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go deleted file mode 100644 index 5cf279144..000000000 --- a/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go +++ /dev/null @@ -1,1109 +0,0 @@ -//go:build integration - -package schema_test - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "github.com/muhlemmer/gu" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/api/grpc" - "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/feature/v2" - "github.com/zitadel/zitadel/pkg/grpc/object/v2" - schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha" -) - -var ( - CTX context.Context - Tester *integration.Tester - Client schema.UserSchemaServiceClient -) - -func TestMain(m *testing.M) { - os.Exit(func() int { - ctx, _, cancel := integration.Contexts(5 * time.Minute) - defer cancel() - - Tester = integration.NewTester(ctx) - defer Tester.Done() - - CTX = Tester.WithAuthorization(ctx, integration.IAMOwner) - Client = Tester.Client.UserSchemaV3 - - return m.Run() - }()) -} - -func ensureFeatureEnabled(t *testing.T) { - f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{ - Inheritance: true, - }) - require.NoError(t, err) - if f.UserSchema.GetEnabled() { - return - } - _, err = Tester.Client.FeatureV2.SetInstanceFeatures(CTX, &feature.SetInstanceFeaturesRequest{ - UserSchema: gu.Ptr(true), - }) - require.NoError(t, err) - retryDuration := time.Minute - if ctxDeadline, ok := CTX.Deadline(); ok { - retryDuration = time.Until(ctxDeadline) - } - require.EventuallyWithT(t, - func(ttt *assert.CollectT) { - f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{ - Inheritance: true, - }) - require.NoError(ttt, err) - if f.UserSchema.GetEnabled() { - return - } - }, - retryDuration, - 100*time.Millisecond, - "timed out waiting for ensuring instance feature") -} - -func TestServer_CreateUserSchema(t *testing.T) { - ensureFeatureEnabled(t) - - tests := []struct { - name string - ctx context.Context - req *schema.CreateUserSchemaRequest - want *schema.CreateUserSchemaResponse - wantErr bool - }{ - { - name: "missing permission, error", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - }, - wantErr: true, - }, - { - name: "empty type", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: "", - }, - wantErr: true, - }, - { - name: "empty schema, error", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - }, - wantErr: true, - }, - { - name: "invalid schema, error", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string", - "required": true - }, - "description": { - "type": "string" - } - } - } - `)) - require.NoError(t, err) - return s - }(), - }, - }, - wantErr: true, - }, - { - name: "no authenticators, ok", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - }, - want: &schema.CreateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "invalid authenticator, error", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED, - }, - }, - wantErr: true, - }, - { - name: "with authenticator, ok", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, - }, - }, - want: &schema.CreateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "with invalid permission, error", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string", - "urn:zitadel:schema:permission": "read" - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, - }, - }, - wantErr: true, - }, - { - name: "with valid permission, ok", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string", - "urn:zitadel:schema:permission": { - "owner": "rw", - "self": "r" - } - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, - }, - }, - want: &schema.CreateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Client.CreateUserSchema(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - assert.NotEmpty(t, got.GetId()) - }) - } -} - -func TestServer_UpdateUserSchema(t *testing.T) { - ensureFeatureEnabled(t) - - type args struct { - ctx context.Context - req *schema.UpdateUserSchemaRequest - } - tests := []struct { - name string - prepare func(request *schema.UpdateUserSchemaRequest) error - args args - want *schema.UpdateUserSchemaResponse - wantErr bool - }{ - { - name: "missing permission, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &schema.UpdateUserSchemaRequest{ - Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), - }, - }, - wantErr: true, - }, - { - name: "missing id, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{}, - }, - wantErr: true, - }, - { - name: "not existing, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - request.Id = "notexisting" - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{}, - }, - wantErr: true, - }, - { - name: "empty type, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - Type: gu.Ptr(""), - }, - }, - wantErr: true, - }, - { - name: "update type, ok", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), - }, - }, - want: &schema.UpdateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "empty schema, ok", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - DataType: &schema.UpdateUserSchemaRequest_Schema{}, - }, - }, - want: &schema.UpdateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "invalid schema, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - DataType: &schema.UpdateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "$schema": "urn:zitadel:schema:v1", - "type": "object", - "properties": { - "name": { - "type": "string", - "required": true - }, - "description": { - "type": "string" - } - } - } - `)) - require.NoError(t, err) - return s - }(), - }, - }, - }, - wantErr: true, - }, - { - name: "update schema, ok", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - DataType: &schema.UpdateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "$schema": "urn:zitadel:schema:v1", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - }, - }, - want: &schema.UpdateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "invalid authenticator, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED, - }, - }, - }, - wantErr: true, - }, - { - name: "update authenticator, ok", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, - }, - }, - }, - want: &schema.UpdateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "inactive, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - _, err := Client.DeactivateUserSchema(CTX, &schema.DeactivateUserSchemaRequest{ - Id: schemaID, - }) - require.NoError(t, err) - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.prepare(tt.args.req) - require.NoError(t, err) - - got, err := Client.UpdateUserSchema(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_DeactivateUserSchema(t *testing.T) { - ensureFeatureEnabled(t) - - type args struct { - ctx context.Context - req *schema.DeactivateUserSchemaRequest - prepare func(request *schema.DeactivateUserSchemaRequest) error - } - tests := []struct { - name string - args args - want *schema.DeactivateUserSchemaResponse - wantErr bool - }{ - { - name: "not existing, error", - args: args{ - CTX, - &schema.DeactivateUserSchemaRequest{ - Id: "notexisting", - }, - func(request *schema.DeactivateUserSchemaRequest) error { return nil }, - }, - wantErr: true, - }, - { - name: "active, ok", - args: args{ - CTX, - &schema.DeactivateUserSchemaRequest{}, - func(request *schema.DeactivateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - }, - want: &schema.DeactivateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "inactive, error", - args: args{ - CTX, - &schema.DeactivateUserSchemaRequest{}, - func(request *schema.DeactivateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - _, err := Client.DeactivateUserSchema(CTX, &schema.DeactivateUserSchemaRequest{ - Id: schemaID, - }) - return err - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.args.prepare(tt.args.req) - require.NoError(t, err) - - got, err := Client.DeactivateUserSchema(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_ReactivateUserSchema(t *testing.T) { - ensureFeatureEnabled(t) - - type args struct { - ctx context.Context - req *schema.ReactivateUserSchemaRequest - prepare func(request *schema.ReactivateUserSchemaRequest) error - } - tests := []struct { - name string - args args - want *schema.ReactivateUserSchemaResponse - wantErr bool - }{ - { - name: "not existing, error", - args: args{ - CTX, - &schema.ReactivateUserSchemaRequest{ - Id: "notexisting", - }, - func(request *schema.ReactivateUserSchemaRequest) error { return nil }, - }, - wantErr: true, - }, - { - name: "active, error", - args: args{ - ctx: CTX, - req: &schema.ReactivateUserSchemaRequest{}, - prepare: func(request *schema.ReactivateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - }, - wantErr: true, - }, - { - name: "inactive, ok", - args: args{ - ctx: CTX, - req: &schema.ReactivateUserSchemaRequest{}, - prepare: func(request *schema.ReactivateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - _, err := Client.DeactivateUserSchema(CTX, &schema.DeactivateUserSchemaRequest{ - Id: schemaID, - }) - return err - }, - }, - want: &schema.ReactivateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.args.prepare(tt.args.req) - require.NoError(t, err) - - got, err := Client.ReactivateUserSchema(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_DeleteUserSchema(t *testing.T) { - ensureFeatureEnabled(t) - - type args struct { - ctx context.Context - req *schema.DeleteUserSchemaRequest - prepare func(request *schema.DeleteUserSchemaRequest) error - } - tests := []struct { - name string - args args - want *schema.DeleteUserSchemaResponse - wantErr bool - }{ - { - name: "not existing, error", - args: args{ - CTX, - &schema.DeleteUserSchemaRequest{ - Id: "notexisting", - }, - func(request *schema.DeleteUserSchemaRequest) error { return nil }, - }, - wantErr: true, - }, - { - name: "delete, ok", - args: args{ - ctx: CTX, - req: &schema.DeleteUserSchemaRequest{}, - prepare: func(request *schema.DeleteUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - }, - want: &schema.DeleteUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "deleted, error", - args: args{ - ctx: CTX, - req: &schema.DeleteUserSchemaRequest{}, - prepare: func(request *schema.DeleteUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - _, err := Client.DeleteUserSchema(CTX, &schema.DeleteUserSchemaRequest{ - Id: schemaID, - }) - return err - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.args.prepare(tt.args.req) - require.NoError(t, err) - - got, err := Client.DeleteUserSchema(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_GetUserSchemaByID(t *testing.T) { - userSchema := new(structpb.Struct) - err := userSchema.UnmarshalJSON([]byte(`{ - "$schema": "urn:zitadel:schema:v1", - "type": "object", - "properties": {} - }`)) - require.NoError(t, err) - type args struct { - ctx context.Context - req *schema.GetUserSchemaByIDRequest - prepare func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error - } - tests := []struct { - name string - args args - want *schema.GetUserSchemaByIDResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &schema.GetUserSchemaByIDRequest{}, - prepare: func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error { - schemaType := fmt.Sprint(time.Now().UnixNano() + 1) - createResp := Tester.CreateUserSchemaWithType(CTX, t, schemaType) - request.Id = createResp.GetId() - return nil - }, - }, - wantErr: true, - }, - { - name: "not existing, error", - args: args{ - ctx: CTX, - req: &schema.GetUserSchemaByIDRequest{ - Id: "notexisting", - }, - }, - wantErr: true, - }, - { - name: "get, ok", - args: args{ - ctx: CTX, - req: &schema.GetUserSchemaByIDRequest{}, - prepare: func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error { - schemaType := fmt.Sprint(time.Now().UnixNano() + 1) - createResp := Tester.CreateUserSchemaWithType(CTX, t, schemaType) - request.Id = createResp.GetId() - - resp.Schema.Id = createResp.GetId() - resp.Schema.Type = schemaType - resp.Schema.Details = &object.Details{ - Sequence: createResp.GetDetails().GetSequence(), - ChangeDate: createResp.GetDetails().GetChangeDate(), - ResourceOwner: createResp.GetDetails().GetResourceOwner(), - } - return nil - }, - }, - want: &schema.GetUserSchemaByIDResponse{ - Schema: &schema.UserSchema{ - State: schema.State_STATE_ACTIVE, - Revision: 1, - Schema: userSchema, - PossibleAuthenticators: nil, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ensureFeatureEnabled(t) - if tt.args.prepare != nil { - err := tt.args.prepare(tt.args.req, tt.want) - require.NoError(t, err) - } - - retryDuration := 5 * time.Second - if ctxDeadline, ok := CTX.Deadline(); ok { - retryDuration = time.Until(ctxDeadline) - } - - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.GetUserSchemaByID(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, err) - return - } - assert.NoError(ttt, err) - - integration.AssertDetails(t, tt.want.GetSchema(), got.GetSchema()) - grpc.AllFieldsEqual(t, tt.want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) - - }, retryDuration, time.Millisecond*100, "timeout waiting for expected user schema result") - }) - } -} - -func TestServer_ListUserSchemas(t *testing.T) { - userSchema := new(structpb.Struct) - err := userSchema.UnmarshalJSON([]byte(`{ - "$schema": "urn:zitadel:schema:v1", - "type": "object", - "properties": {} - }`)) - require.NoError(t, err) - type args struct { - ctx context.Context - req *schema.ListUserSchemasRequest - prepare func(request *schema.ListUserSchemasRequest, resp *schema.ListUserSchemasResponse) error - } - tests := []struct { - name string - args args - want *schema.ListUserSchemasResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &schema.ListUserSchemasRequest{}, - }, - wantErr: true, - }, - { - name: "not found, error", - args: args{ - ctx: CTX, - req: &schema.ListUserSchemasRequest{ - Queries: []*schema.SearchQuery{ - { - Query: &schema.SearchQuery_IdQuery{ - IdQuery: &schema.IDQuery{ - Id: "notexisting", - }, - }, - }, - }, - }, - }, - want: &schema.ListUserSchemasResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - }, - Result: []*schema.UserSchema{}, - }, - }, - { - name: "single (id), ok", - args: args{ - ctx: CTX, - req: &schema.ListUserSchemasRequest{}, - prepare: func(request *schema.ListUserSchemasRequest, resp *schema.ListUserSchemasResponse) error { - schemaType := fmt.Sprint(time.Now().UnixNano() + 1) - createResp := Tester.CreateUserSchemaWithType(CTX, t, schemaType) - request.Queries = []*schema.SearchQuery{ - { - Query: &schema.SearchQuery_IdQuery{ - IdQuery: &schema.IDQuery{ - Id: createResp.GetId(), - Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, - }, - }, - }, - } - - resp.Result[0].Id = createResp.GetId() - resp.Result[0].Type = schemaType - resp.Result[0].Details = &object.Details{ - Sequence: createResp.GetDetails().GetSequence(), - ChangeDate: createResp.GetDetails().GetChangeDate(), - ResourceOwner: createResp.GetDetails().GetResourceOwner(), - } - return nil - }, - }, - want: &schema.ListUserSchemasResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - }, - Result: []*schema.UserSchema{ - { - State: schema.State_STATE_ACTIVE, - Revision: 1, - Schema: userSchema, - PossibleAuthenticators: nil, - }, - }, - }, - }, - { - name: "multiple (type), ok", - args: args{ - ctx: CTX, - req: &schema.ListUserSchemasRequest{}, - prepare: func(request *schema.ListUserSchemasRequest, resp *schema.ListUserSchemasResponse) error { - schemaType := fmt.Sprint(time.Now().UnixNano()) - schemaType1 := schemaType + "_1" - schemaType2 := schemaType + "_2" - createResp := Tester.CreateUserSchemaWithType(CTX, t, schemaType1) - createResp2 := Tester.CreateUserSchemaWithType(CTX, t, schemaType2) - - request.SortingColumn = schema.FieldName_FIELD_NAME_TYPE - request.Query = &object.ListQuery{Asc: true} - request.Queries = []*schema.SearchQuery{ - { - Query: &schema.SearchQuery_TypeQuery{ - TypeQuery: &schema.TypeQuery{ - Type: schemaType, - Method: object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH, - }, - }, - }, - } - - resp.Result[0].Id = createResp.GetId() - resp.Result[0].Type = schemaType1 - resp.Result[0].Details = &object.Details{ - Sequence: createResp.GetDetails().GetSequence(), - ChangeDate: createResp.GetDetails().GetChangeDate(), - ResourceOwner: createResp.GetDetails().GetResourceOwner(), - } - resp.Result[1].Id = createResp2.GetId() - resp.Result[1].Type = schemaType2 - resp.Result[1].Details = &object.Details{ - Sequence: createResp2.GetDetails().GetSequence(), - ChangeDate: createResp2.GetDetails().GetChangeDate(), - ResourceOwner: createResp2.GetDetails().GetResourceOwner(), - } - return nil - }, - }, - want: &schema.ListUserSchemasResponse{ - Details: &object.ListDetails{ - TotalResult: 2, - }, - Result: []*schema.UserSchema{ - { - State: schema.State_STATE_ACTIVE, - Revision: 1, - Schema: userSchema, - PossibleAuthenticators: nil, - }, - { - State: schema.State_STATE_ACTIVE, - Revision: 1, - Schema: userSchema, - PossibleAuthenticators: nil, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ensureFeatureEnabled(t) - if tt.args.prepare != nil { - err := tt.args.prepare(tt.args.req, tt.want) - require.NoError(t, err) - } - - retryDuration := 20 * time.Second - if ctxDeadline, ok := CTX.Deadline(); ok { - retryDuration = time.Until(ctxDeadline) - } - - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.ListUserSchemas(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, err) - return - } - assert.NoError(ttt, err) - - // always first check length, otherwise its failed anyway - assert.Len(ttt, got.Result, len(tt.want.Result)) - for i := range tt.want.Result { - // - grpc.AllFieldsEqual(t, tt.want.Result[i].ProtoReflect(), got.Result[i].ProtoReflect(), grpc.CustomMappers) - } - integration.AssertListDetails(t, tt.want, got) - }, retryDuration, time.Millisecond*100, "timeout waiting for expected user schema result") - }) - } -} diff --git a/internal/api/grpc/user/schema/v3alpha/server.go b/internal/api/grpc/user/schema/v3alpha/server.go deleted file mode 100644 index c02bb7c62..000000000 --- a/internal/api/grpc/user/schema/v3alpha/server.go +++ /dev/null @@ -1,51 +0,0 @@ -package schema - -import ( - "google.golang.org/grpc" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/grpc/server" - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/query" - schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha" -) - -var _ schema.UserSchemaServiceServer = (*Server)(nil) - -type Server struct { - schema.UnimplementedUserSchemaServiceServer - command *command.Commands - query *query.Queries -} - -type Config struct{} - -func CreateServer( - command *command.Commands, - query *query.Queries, -) *Server { - return &Server{ - command: command, - query: query, - } -} - -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - schema.RegisterUserSchemaServiceServer(grpcServer, s) -} - -func (s *Server) AppName() string { - return schema.UserSchemaService_ServiceDesc.ServiceName -} - -func (s *Server) MethodPrefix() string { - return schema.UserSchemaService_ServiceDesc.ServiceName -} - -func (s *Server) AuthMethods() authz.MethodMapping { - return schema.UserSchemaService_AuthMethods -} - -func (s *Server) RegisterGateway() server.RegisterGatewayFunc { - return schema.RegisterUserSchemaServiceHandler -} diff --git a/internal/command/user_schema.go b/internal/command/user_schema.go index 507e0cace..7b53c9e16 100644 --- a/internal/command/user_schema.go +++ b/internal/command/user_schema.go @@ -12,6 +12,8 @@ import ( ) type CreateUserSchema struct { + Details *domain.ObjectDetails + ResourceOwner string Type string Schema json.RawMessage @@ -33,7 +35,9 @@ func (s *CreateUserSchema) Valid() error { return nil } -type UpdateUserSchema struct { +type ChangeUserSchema struct { + Details *domain.ObjectDetails + ID string ResourceOwner string Type *string @@ -41,7 +45,7 @@ type UpdateUserSchema struct { PossibleAuthenticators []domain.AuthenticatorType } -func (s *UpdateUserSchema) Valid() error { +func (s *ChangeUserSchema) Valid() error { if s.ID == "" { return zerrors.ThrowInvalidArgument(nil, "COMMA-H5421", "Errors.IDMissing") } @@ -59,40 +63,43 @@ func (s *UpdateUserSchema) Valid() error { return nil } -func (c *Commands) CreateUserSchema(ctx context.Context, userSchema *CreateUserSchema) (string, *domain.ObjectDetails, error) { +func (c *Commands) CreateUserSchema(ctx context.Context, userSchema *CreateUserSchema) error { if err := userSchema.Valid(); err != nil { - return "", nil, err + return err } if userSchema.ResourceOwner == "" { - return "", nil, zerrors.ThrowInvalidArgument(nil, "COMMA-J3hhj", "Errors.ResourceOwnerMissing") + return zerrors.ThrowInvalidArgument(nil, "COMMA-J3hhj", "Errors.ResourceOwnerMissing") } id, err := c.idGenerator.Next() if err != nil { - return "", nil, err + return err } - writeModel := NewUserSchemaWriteModel(id, userSchema.ResourceOwner) - err = c.pushAppendAndReduce(ctx, writeModel, + writeModel, err := c.getSchemaWriteModelByID(ctx, userSchema.ResourceOwner, id) + if err != nil { + return err + } + if err := c.pushAppendAndReduce(ctx, writeModel, schema.NewCreatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel), userSchema.Type, userSchema.Schema, userSchema.PossibleAuthenticators, ), - ) - if err != nil { - return "", nil, err + ); err != nil { + return err } - return id, writeModelToObjectDetails(&writeModel.WriteModel), nil + userSchema.Details = writeModelToObjectDetails(&writeModel.WriteModel) + return nil } -func (c *Commands) UpdateUserSchema(ctx context.Context, userSchema *UpdateUserSchema) (*domain.ObjectDetails, error) { +func (c *Commands) ChangeUserSchema(ctx context.Context, userSchema *ChangeUserSchema) error { if err := userSchema.Valid(); err != nil { - return nil, err + return err } - writeModel := NewUserSchemaWriteModel(userSchema.ID, userSchema.ResourceOwner) - if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { - return nil, err + writeModel, err := c.getSchemaWriteModelByID(ctx, userSchema.ResourceOwner, userSchema.ID) + if err != nil { + return err } if writeModel.State != domain.UserSchemaStateActive { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-HB3e1", "Errors.UserSchema.NotActive") + return zerrors.ThrowPreconditionFailed(nil, "COMMA-HB3e1", "Errors.UserSchema.NotActive") } updatedEvent := writeModel.NewUpdatedEvent( ctx, @@ -102,29 +109,30 @@ func (c *Commands) UpdateUserSchema(ctx context.Context, userSchema *UpdateUserS userSchema.PossibleAuthenticators, ) if updatedEvent == nil { - return writeModelToObjectDetails(&writeModel.WriteModel), nil + userSchema.Details = writeModelToObjectDetails(&writeModel.WriteModel) + return nil } if err := c.pushAppendAndReduce(ctx, writeModel, updatedEvent); err != nil { - return nil, err + return err } - return writeModelToObjectDetails(&writeModel.WriteModel), nil + userSchema.Details = writeModelToObjectDetails(&writeModel.WriteModel) + return nil } func (c *Commands) DeactivateUserSchema(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) { if id == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-Vvf3w", "Errors.IDMissing") } - writeModel := NewUserSchemaWriteModel(id, resourceOwner) - if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { + writeModel, err := c.getSchemaWriteModelByID(ctx, resourceOwner, id) + if err != nil { return nil, err } if writeModel.State != domain.UserSchemaStateActive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-E4t4z", "Errors.UserSchema.NotActive") } - err := c.pushAppendAndReduce(ctx, writeModel, + if err := c.pushAppendAndReduce(ctx, writeModel, schema.NewDeactivatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel)), - ) - if err != nil { + ); err != nil { return nil, err } return writeModelToObjectDetails(&writeModel.WriteModel), nil @@ -134,17 +142,16 @@ func (c *Commands) ReactivateUserSchema(ctx context.Context, id, resourceOwner s if id == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-wq3Gw", "Errors.IDMissing") } - writeModel := NewUserSchemaWriteModel(id, resourceOwner) - if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { + writeModel, err := c.getSchemaWriteModelByID(ctx, resourceOwner, id) + if err != nil { return nil, err } if writeModel.State != domain.UserSchemaStateInactive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-DGzh5", "Errors.UserSchema.NotInactive") } - err := c.pushAppendAndReduce(ctx, writeModel, + if err := c.pushAppendAndReduce(ctx, writeModel, schema.NewReactivatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel)), - ) - if err != nil { + ); err != nil { return nil, err } return writeModelToObjectDetails(&writeModel.WriteModel), nil @@ -154,18 +161,17 @@ func (c *Commands) DeleteUserSchema(ctx context.Context, id, resourceOwner strin if id == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-E22gg", "Errors.IDMissing") } - writeModel := NewUserSchemaWriteModel(id, resourceOwner) - if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { + writeModel, err := c.getSchemaWriteModelByID(ctx, resourceOwner, id) + if err != nil { return nil, err } if !writeModel.Exists() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-Grg41", "Errors.UserSchema.NotExists") } // TODO: check for users based on that schema; this is only possible with / after https://github.com/zitadel/zitadel/issues/7308 - err := c.pushAppendAndReduce(ctx, writeModel, + if err := c.pushAppendAndReduce(ctx, writeModel, schema.NewDeletedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel), writeModel.SchemaType), - ) - if err != nil { + ); err != nil { return nil, err } return writeModelToObjectDetails(&writeModel.WriteModel), nil @@ -178,3 +184,11 @@ func validateUserSchema(userSchema json.RawMessage) error { } return nil } + +func (c *Commands) getSchemaWriteModelByID(ctx context.Context, resourceOwner, id string) (*UserSchemaWriteModel, error) { + writeModel := NewUserSchemaWriteModel(resourceOwner, id, "") + if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { + return nil, err + } + return writeModel, nil +} diff --git a/internal/command/user_schema_model.go b/internal/command/user_schema_model.go index ccb5fbf27..e8df80479 100644 --- a/internal/command/user_schema_model.go +++ b/internal/command/user_schema_model.go @@ -19,14 +19,16 @@ type UserSchemaWriteModel struct { Schema json.RawMessage PossibleAuthenticators []domain.AuthenticatorType State domain.UserSchemaState + Revision uint64 } -func NewUserSchemaWriteModel(schemaID, resourceOwner string) *UserSchemaWriteModel { +func NewUserSchemaWriteModel(resourceOwner, schemaID, ty string) *UserSchemaWriteModel { return &UserSchemaWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: schemaID, ResourceOwner: resourceOwner, }, + SchemaType: ty, } } @@ -38,10 +40,14 @@ func (wm *UserSchemaWriteModel) Reduce() error { wm.Schema = e.Schema wm.PossibleAuthenticators = e.PossibleAuthenticators wm.State = domain.UserSchemaStateActive + wm.Revision = 1 case *schema.UpdatedEvent: if e.SchemaType != nil { wm.SchemaType = *e.SchemaType } + if e.SchemaRevision != nil { + wm.Revision = *e.SchemaRevision + } if len(e.Schema) > 0 { wm.Schema = e.Schema } @@ -60,7 +66,7 @@ func (wm *UserSchemaWriteModel) Reduce() error { } func (wm *UserSchemaWriteModel) Query() *eventstore.SearchQueryBuilder { - return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(wm.ResourceOwner). AddQuery(). AggregateTypes(schema.AggregateType). @@ -71,8 +77,13 @@ func (wm *UserSchemaWriteModel) Query() *eventstore.SearchQueryBuilder { schema.DeactivatedType, schema.ReactivatedType, schema.DeletedType, - ). - Builder() + ) + + if wm.SchemaType != "" { + query = query.EventData(map[string]interface{}{"schemaType": wm.SchemaType}) + } + + return query.Builder() } func (wm *UserSchemaWriteModel) NewUpdatedEvent( ctx context.Context, @@ -87,6 +98,8 @@ 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)) } if len(possibleAuthenticators) > 0 && slices.Compare(wm.PossibleAuthenticators, possibleAuthenticators) != 0 { changes = append(changes, schema.ChangePossibleAuthenticators(possibleAuthenticators)) diff --git a/internal/command/user_schema_test.go b/internal/command/user_schema_test.go index 084963116..e9f06bff2 100644 --- a/internal/command/user_schema_test.go +++ b/internal/command/user_schema_test.go @@ -27,7 +27,6 @@ func TestCommands_CreateUserSchema(t *testing.T) { userSchema *CreateUserSchema } type res struct { - id string details *domain.ObjectDetails err error } @@ -107,6 +106,7 @@ func TestCommands_CreateUserSchema(t *testing.T) { "empty user schema created", fields{ eventstore: expectEventstore( + expectFilter(), expectPush( schema.NewCreatedEvent( context.Background(), @@ -131,8 +131,8 @@ func TestCommands_CreateUserSchema(t *testing.T) { }, }, res{ - id: "id1", details: &domain.ObjectDetails{ + ID: "id1", ResourceOwner: "instanceID", }, }, @@ -141,6 +141,7 @@ func TestCommands_CreateUserSchema(t *testing.T) { "user schema created", fields{ eventstore: expectEventstore( + expectFilter(), expectPush( schema.NewCreatedEvent( context.Background(), @@ -181,8 +182,8 @@ func TestCommands_CreateUserSchema(t *testing.T) { }, }, res{ - id: "id1", details: &domain.ObjectDetails{ + ID: "id1", ResourceOwner: "instanceID", }, }, @@ -220,6 +221,7 @@ func TestCommands_CreateUserSchema(t *testing.T) { "user schema with permission created", fields{ eventstore: expectEventstore( + expectFilter(), expectPush( schema.NewCreatedEvent( context.Background(), @@ -266,8 +268,8 @@ func TestCommands_CreateUserSchema(t *testing.T) { }, }, res{ - id: "id1", details: &domain.ObjectDetails{ + ID: "id1", ResourceOwner: "instanceID", }, }, @@ -279,21 +281,20 @@ func TestCommands_CreateUserSchema(t *testing.T) { eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } - gotID, gotDetails, err := c.CreateUserSchema(tt.args.ctx, tt.args.userSchema) - assert.Equal(t, tt.res.id, gotID) - assertObjectDetails(t, tt.res.details, gotDetails) + err := c.CreateUserSchema(tt.args.ctx, tt.args.userSchema) + assertObjectDetails(t, tt.res.details, tt.args.userSchema.Details) assert.ErrorIs(t, err, tt.res.err) }) } } -func TestCommands_UpdateUserSchema(t *testing.T) { +func TestCommands_ChangeUserSchema(t *testing.T) { type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context - userSchema *UpdateUserSchema + userSchema *ChangeUserSchema } type res struct { details *domain.ObjectDetails @@ -312,7 +313,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{}, + userSchema: &ChangeUserSchema{}, }, res{ err: zerrors.ThrowInvalidArgument(nil, "COMMA-H5421", "Errors.IDMissing"), @@ -325,7 +326,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Type: gu.Ptr(""), }, @@ -341,7 +342,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", }, }, @@ -356,7 +357,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Schema: json.RawMessage(`{ "properties": { @@ -379,7 +380,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Schema: json.RawMessage(`{}`), PossibleAuthenticators: []domain.AuthenticatorType{ @@ -400,7 +401,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Type: gu.Ptr("type"), Schema: json.RawMessage(`{}`), @@ -432,7 +433,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Type: gu.Ptr("type"), Schema: json.RawMessage(`{}`), @@ -473,7 +474,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Schema: json.RawMessage(`{}`), Type: gu.Ptr("newType"), @@ -515,7 +516,9 @@ func TestCommands_UpdateUserSchema(t *testing.T) { schema.NewUpdatedEvent( context.Background(), &schema.NewAggregate("id1", "instanceID").Aggregate, - []schema.Changes{schema.ChangeSchema(json.RawMessage(`{ + []schema.Changes{ + schema.IncreaseRevision(1), + schema.ChangeSchema(json.RawMessage(`{ "$schema": "urn:zitadel:schema:v1", "type": "object", "properties": { @@ -539,7 +542,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Schema: json.RawMessage(`{ "$schema": "urn:zitadel:schema:v1", @@ -597,7 +600,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Schema: json.RawMessage(`{}`), PossibleAuthenticators: []domain.AuthenticatorType{ @@ -618,9 +621,9 @@ func TestCommands_UpdateUserSchema(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore(t), } - got, err := c.UpdateUserSchema(tt.args.ctx, tt.args.userSchema) + err := c.ChangeUserSchema(tt.args.ctx, tt.args.userSchema) assert.ErrorIs(t, err, tt.res.err) - assertObjectDetails(t, tt.res.details, got) + assertObjectDetails(t, tt.res.details, tt.args.userSchema.Details) }) } } diff --git a/internal/command/user_v3.go b/internal/command/user_v3.go new file mode 100644 index 000000000..f2cacd6cb --- /dev/null +++ b/internal/command/user_v3.go @@ -0,0 +1,220 @@ +package command + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + domain_schema "github.com/zitadel/zitadel/internal/domain/schema" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user/schemauser" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type CreateSchemaUser struct { + Details *domain.ObjectDetails + ResourceOwner string + + SchemaID string + schemaRevision uint64 + + ID string + Data json.RawMessage + + Email *Email + ReturnCodeEmail string + Phone *Phone + ReturnCodePhone string +} + +func (s *CreateSchemaUser) Valid(ctx context.Context, c *Commands) (err error) { + if s.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-urEJKa1tJM", "Errors.ResourceOwnerMissing") + } + if s.SchemaID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-TFo06JgnF2", "Errors.UserSchema.ID.Missing") + } + + schemaWriteModel, err := c.getSchemaWriteModelByID(ctx, "", s.SchemaID) + if err != nil { + return err + } + if !schemaWriteModel.Exists() { + return zerrors.ThrowPreconditionFailed(nil, "COMMAND-N9QOuN4F7o", "Errors.UserSchema.NotExists") + } + s.schemaRevision = schemaWriteModel.Revision + + if s.ID == "" { + s.ID, err = c.idGenerator.Next() + if err != nil { + return err + } + } + + // get role for permission check in schema through extension + role, err := c.getSchemaRoleForWrite(ctx, s.ResourceOwner, s.ID) + if err != nil { + return err + } + + schema, err := domain_schema.NewSchema(role, bytes.NewReader(schemaWriteModel.Schema)) + if err != nil { + return err + } + + var v interface{} + if err := json.Unmarshal(s.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") + } + + 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 (c *Commands) getSchemaRoleForWrite(ctx context.Context, resourceOwner, userID string) (domain_schema.Role, error) { + if userID == authz.GetCtxData(ctx).UserID { + return domain_schema.RoleSelf, nil + } + if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil { + return domain_schema.RoleUnspecified, err + } + return domain_schema.RoleOwner, nil +} + +func (c *Commands) CreateSchemaUser(ctx context.Context, user *CreateSchemaUser, alg crypto.EncryptionAlgorithm) (err error) { + if err := user.Valid(ctx, c); err != nil { + return err + } + + writeModel, err := c.getSchemaUserExists(ctx, user.ResourceOwner, user.ID) + if err != nil { + return err + } + if writeModel.Exists() { + return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.AlreadyExists") + } + + userAgg := UserV3AggregateFromWriteModel(&writeModel.WriteModel) + events := []eventstore.Command{ + schemauser.NewCreatedEvent(ctx, + userAgg, + user.SchemaID, user.schemaRevision, user.Data, + ), + } + if user.Email != nil { + events, user.ReturnCodeEmail, err = c.updateSchemaUserEmail(ctx, 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) + if err != nil { + return err + } + } + + if err := c.pushAppendAndReduce(ctx, writeModel, events...); err != nil { + return err + } + user.Details = writeModelToObjectDetails(&writeModel.WriteModel) + return nil +} + +func (c *Commands) DeleteSchemaUser(ctx context.Context, id string) (*domain.ObjectDetails, error) { + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Vs4wJCME7T", "Errors.IDMissing") + } + writeModel, err := c.getSchemaUserExists(ctx, "", id) + if err != nil { + return nil, err + } + if !writeModel.Exists() { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-syHyCsGmvM", "Errors.User.NotFound") + } + if err := c.checkPermissionDeleteUser(ctx, writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + if err := c.pushAppendAndReduce(ctx, writeModel, + schemauser.NewDeletedEvent(ctx, UserV3AggregateFromWriteModel(&writeModel.WriteModel)), + ); err != nil { + return nil, err + } + return writeModelToObjectDetails(&writeModel.WriteModel), nil +} + +func (c *Commands) updateSchemaUserEmail(ctx context.Context, events []eventstore.Command, agg *eventstore.Aggregate, email *Email, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) { + + events = append(events, schemauser.NewEmailUpdatedEvent(ctx, + agg, + email.Address, + )) + if email.Verified { + events = append(events, schemauser.NewEmailVerifiedEvent(ctx, agg)) + } else { + cryptoCode, err := c.newEmailCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck + if err != nil { + return nil, "", err + } + if email.ReturnCode { + plainCode = cryptoCode.Plain + } + events = append(events, schemauser.NewEmailCodeAddedEvent(ctx, agg, + cryptoCode.Crypted, + cryptoCode.Expiry, + email.URLTemplate, + email.ReturnCode, + )) + } + return events, plainCode, nil +} + +func (c *Commands) updateSchemaUserPhone(ctx context.Context, events []eventstore.Command, agg *eventstore.Aggregate, phone *Phone, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) { + events = append(events, schemauser.NewPhoneChangedEvent(ctx, + agg, + phone.Number, + )) + if phone.Verified { + events = append(events, schemauser.NewPhoneVerifiedEvent(ctx, agg)) + } else { + cryptoCode, err := c.newPhoneCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck + if err != nil { + return nil, "", err + } + if phone.ReturnCode { + plainCode = cryptoCode.Plain + } + events = append(events, schemauser.NewPhoneCodeAddedEvent(ctx, agg, + cryptoCode.Crypted, + cryptoCode.Expiry, + phone.ReturnCode, + )) + } + return events, plainCode, nil +} + +func (c *Commands) getSchemaUserExists(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) { + writeModel := NewExistsUserV3WriteModel(resourceOwner, id) + 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 new file mode 100644 index 000000000..51f783aae --- /dev/null +++ b/internal/command/user_v3_model.go @@ -0,0 +1,174 @@ +package command + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user/schemauser" +) + +type UserV3WriteModel struct { + eventstore.WriteModel + + PhoneWM bool + EmailWM bool + DataWM bool + + SchemaID string + SchemaRevision uint64 + + Email string + IsEmailVerified bool + EmailVerifiedFailedCount int + Phone string + IsPhoneVerified bool + PhoneVerifiedFailedCount int + + Data json.RawMessage + + State domain.UserState +} + +func NewExistsUserV3WriteModel(resourceOwner, userID string) *UserV3WriteModel { + return &UserV3WriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: userID, + ResourceOwner: resourceOwner, + }, + PhoneWM: false, + EmailWM: false, + DataWM: false, + } +} + +func NewUserV3WriteModel(resourceOwner, userID string) *UserV3WriteModel { + return &UserV3WriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: userID, + ResourceOwner: resourceOwner, + }, + PhoneWM: true, + EmailWM: true, + DataWM: true, + } +} + +func (wm *UserV3WriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *schemauser.CreatedEvent: + wm.SchemaID = e.SchemaID + wm.SchemaRevision = 1 + wm.Data = e.Data + + wm.State = domain.UserStateActive + case *schemauser.UpdatedEvent: + if e.SchemaID != nil { + wm.SchemaID = *e.SchemaID + } + if e.SchemaRevision != nil { + wm.SchemaRevision = *e.SchemaRevision + } + if len(e.Data) > 0 { + wm.Data = e.Data + } + case *schemauser.DeletedEvent: + wm.State = domain.UserStateDeleted + case *schemauser.EmailUpdatedEvent: + wm.Email = string(e.EmailAddress) + case *schemauser.EmailCodeAddedEvent: + wm.IsEmailVerified = false + wm.EmailVerifiedFailedCount = 0 + case *schemauser.EmailVerifiedEvent: + wm.IsEmailVerified = true + wm.EmailVerifiedFailedCount = 0 + case *schemauser.EmailVerificationFailedEvent: + wm.EmailVerifiedFailedCount += 1 + case *schemauser.PhoneChangedEvent: + wm.Phone = string(e.PhoneNumber) + case *schemauser.PhoneCodeAddedEvent: + wm.IsPhoneVerified = false + wm.PhoneVerifiedFailedCount = 0 + case *schemauser.PhoneVerifiedEvent: + wm.PhoneVerifiedFailedCount = 0 + wm.IsPhoneVerified = true + case *schemauser.PhoneVerificationFailedEvent: + wm.PhoneVerifiedFailedCount += 1 + } + } + 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, + ) + if wm.DataWM { + query = query.EventTypes( + schemauser.UpdatedType, + ) + } + if wm.EmailWM { + query = query.EventTypes( + schemauser.EmailUpdatedType, + schemauser.EmailVerifiedType, + schemauser.EmailCodeAddedType, + schemauser.EmailVerificationFailedType, + ) + } + if wm.PhoneWM { + query = query.EventTypes( + schemauser.PhoneUpdatedType, + schemauser.PhoneVerifiedType, + schemauser.PhoneCodeAddedType, + schemauser.PhoneVerificationFailedType, + ) + } + return query.Builder() +} + +func (wm *UserV3WriteModel) NewUpdatedEvent( + ctx context.Context, + agg *eventstore.Aggregate, + 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 schemaRevision != nil && wm.SchemaRevision != *schemaRevision { + changes = append(changes, schemauser.ChangeSchemaRevision(wm.SchemaRevision, *schemaRevision)) + } + if !bytes.Equal(wm.Data, data) { + changes = append(changes, schemauser.ChangeData(data)) + } + if len(changes) == 0 { + return nil + } + return schemauser.NewUpdatedEvent(ctx, agg, changes) +} + +func UserV3AggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { + return &eventstore.Aggregate{ + ID: wm.AggregateID, + Type: schemauser.AggregateType, + ResourceOwner: wm.ResourceOwner, + InstanceID: wm.InstanceID, + Version: schemauser.AggregateVersion, + } +} + +func (wm *UserV3WriteModel) Exists() bool { + return wm.State != domain.UserStateDeleted && wm.State != domain.UserStateUnspecified +} diff --git a/internal/command/user_v3_test.go b/internal/command/user_v3_test.go new file mode 100644 index 000000000..5bbb9e0c5 --- /dev/null +++ b/internal/command/user_v3_test.go @@ -0,0 +1,1103 @@ +package command + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/id" + "github.com/zitadel/zitadel/internal/id/mock" + "github.com/zitadel/zitadel/internal/repository/user/schema" + "github.com/zitadel/zitadel/internal/repository/user/schemauser" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommands_CreateSchemaUser(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + checkPermission domain.PermissionCheck + newCode encrypedCodeFunc + } + type args struct { + ctx context.Context + user *CreateSchemaUser + } + 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 resourceOwner, error", + fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-urEJKa1tJM", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + "no schemaID, error", + fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-TFo06JgnF2", "Errors.UserSchema.ID.Missing")) + }, + }, + }, + { + "schema not existing, error", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-N9QOuN4F7o", "Errors.UserSchema.NotExists")) + }, + }, + }, + { + "no data, error", + 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}, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-7o3ZGxtXUz", "Errors.User.Invalid")) + }, + }, + }, + { + "user create, no permission", + 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}, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + "user created", + 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(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + }, + }, + { + "user create, 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}, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + 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 create, 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}, + ), + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "org1", "id1"), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + 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 create, 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}, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "org1", "user1"), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + 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 created, 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(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "additional": "property" + }`), + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "additional": "property" + }`), + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + }, + }, + { + "user create, 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}, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "org1", "user1"), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + 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 created, 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(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + schemauser.NewEmailUpdatedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "test@example.com", + ), + schemauser.NewEmailCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("id1", "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, + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("emailverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + 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", + ID: "id1", + }, + returnCodeEmail: "emailverify", + }, + }, + { + "user created, email to verify", + 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(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + schemauser.NewEmailUpdatedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "test@example.com", + ), + schemauser.NewEmailCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("id1", "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, + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("emailverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + Email: &Email{ + Address: "test@example.com", + URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + }, + }, + { + "user created, phone 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(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + schemauser.NewPhoneChangedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("phoneverify"), + }, + time.Hour*1, + true, + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("phoneverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + Phone: &Phone{ + Number: "+41791234567", + ReturnCode: true, + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + returnCodePhone: "phoneverify", + }, + }, + { + "user created, phone to verify", + 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(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + schemauser.NewPhoneChangedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("phoneverify"), + }, + time.Hour*1, + false, + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("phoneverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + Phone: &Phone{ + Number: "+41791234567", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + }, + }, + { + "user created, full verified", + 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(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + schemauser.NewEmailUpdatedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "test@example.com", + ), + schemauser.NewEmailVerifiedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + ), + schemauser.NewPhoneChangedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneVerifiedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + Email: &Email{Address: "test@example.com", Verified: true}, + Phone: &Phone{Number: "+41791234567", Verified: true}, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + checkPermission: tt.fields.checkPermission, + newEncryptedCode: tt.fields.newCode, + } + err := c.CreateSchemaUser(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) + } + }) + } +} + +func TestCommandSide_DeleteSchemaUser(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type ( + args struct { + ctx context.Context + 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-Vs4wJCME7T", "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-syHyCsGmvM", "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-syHyCsGmvM", "Errors.User.NotFound")) + }, + }, + }, + { + name: "remove 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.NewDeletedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "remove 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")) + }, + }, + }, + { + name: "remove user, self", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + expectPush( + schemauser.NewDeletedEvent(authz.NewMockContext("instanceID", "org1", "user1"), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContext("instanceID", "org1", "user1"), + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + 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.DeleteSchemaUser(tt.args.ctx, 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) + } + }) + } +} diff --git a/internal/domain/schema/permission.go b/internal/domain/schema/permission.go index deb33ab14..4a2fdee3c 100644 --- a/internal/domain/schema/permission.go +++ b/internal/domain/schema/permission.go @@ -20,16 +20,16 @@ const ( PermissionProperty = "urn:zitadel:schema:permission" ) -type role int32 +type Role int32 const ( - roleUnspecified role = iota - roleSelf - roleOwner + RoleUnspecified Role = iota + RoleSelf + RoleOwner ) type permissionExtension struct { - role role + role Role } // Compile implements the [jsonschema.ExtCompiler] interface. @@ -57,14 +57,14 @@ func (c permissionExtension) Compile(ctx jsonschema.CompilerContext, m map[strin return } default: - return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission role") + return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission Role") } } return permissionExtensionConfig{c.role, perms}, nil } type permissionExtensionConfig struct { - role role + role Role permissions *permissions } @@ -72,17 +72,17 @@ type permissionExtensionConfig struct { // It validates the fields of the json instance according to the permission schema. func (s permissionExtensionConfig) Validate(ctx jsonschema.ValidationContext, v interface{}) error { switch s.role { - case roleSelf: + case RoleSelf: if s.permissions.self == nil || !s.permissions.self.write { return ctx.Error("permission", "missing required permission") } return nil - case roleOwner: + case RoleOwner: if s.permissions.owner == nil || !s.permissions.owner.write { return ctx.Error("permission", "missing required permission") } return nil - case roleUnspecified: + case RoleUnspecified: fallthrough default: return ctx.Error("permission", "missing required permission") diff --git a/internal/domain/schema/permission_test.go b/internal/domain/schema/permission_test.go index b0799384b..11b665078 100644 --- a/internal/domain/schema/permission_test.go +++ b/internal/domain/schema/permission_test.go @@ -14,7 +14,7 @@ import ( func TestPermissionExtension(t *testing.T) { type args struct { - role role + role Role schema string instance string } @@ -83,7 +83,7 @@ func TestPermissionExtension(t *testing.T) { }, }, { - "invalid role, compilation err", + "invalid Role, compilation err", args{ schema: `{ "type": "object", @@ -98,13 +98,13 @@ func TestPermissionExtension(t *testing.T) { }`, }, want{ - compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission role"), + compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission Role"), }, }, { "invalid permission self, validation err", args{ - role: roleSelf, + role: RoleSelf, schema: `{ "type": "object", "properties": { @@ -126,7 +126,7 @@ func TestPermissionExtension(t *testing.T) { { "invalid permission owner, validation err", args{ - role: roleOwner, + role: RoleOwner, schema: `{ "type": "object", "properties": { @@ -148,7 +148,7 @@ func TestPermissionExtension(t *testing.T) { { "valid permission self, ok", args{ - role: roleSelf, + role: RoleSelf, schema: `{ "type": "object", "properties": { @@ -170,7 +170,7 @@ func TestPermissionExtension(t *testing.T) { { "valid permission owner, ok", args{ - role: roleOwner, + role: RoleOwner, schema: `{ "type": "object", "properties": { @@ -190,9 +190,9 @@ func TestPermissionExtension(t *testing.T) { }, }, { - "no role, validation err", + "no Role, validation err", args{ - role: roleUnspecified, + role: RoleUnspecified, schema: `{ "type": "object", "properties": { @@ -214,7 +214,7 @@ func TestPermissionExtension(t *testing.T) { { "no permission required, ok", args{ - role: roleSelf, + role: RoleSelf, schema: `{ "type": "object", "properties": { diff --git a/internal/domain/schema/schema.go b/internal/domain/schema/schema.go index a8eee88de..126d7473c 100644 --- a/internal/domain/schema/schema.go +++ b/internal/domain/schema/schema.go @@ -19,7 +19,7 @@ const ( MetaSchemaID = "urn:zitadel:schema:v1" ) -func NewSchema(role role, r io.Reader) (*jsonschema.Schema, error) { +func NewSchema(role Role, r io.Reader) (*jsonschema.Schema, error) { c := jsonschema.NewCompiler() if err := c.AddResource(PermissionSchemaID, strings.NewReader(permissionJSON)); err != nil { return nil, err @@ -31,11 +31,11 @@ func NewSchema(role role, r io.Reader) (*jsonschema.Schema, error) { role, }) if err := c.AddResource("schema.json", r); err != nil { - return nil, zerrors.ThrowInvalidArgument(err, "COMMA-Frh42", "Errors.UserSchema.Schema.Invalid") + return nil, zerrors.ThrowInvalidArgument(err, "COMMA-Frh42", "Errors.UserSchema.Invalid") } schema, err := c.Compile("schema.json") if err != nil { - return nil, zerrors.ThrowInvalidArgument(err, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid") + return nil, zerrors.ThrowInvalidArgument(err, "COMMA-W21tg", "Errors.UserSchema.Invalid") } return schema, nil } diff --git a/internal/execution/execution_test.go b/internal/execution/execution_test.go index c2c6198ed..184823f9b 100644 --- a/internal/execution/execution_test.go +++ b/internal/execution/execution_test.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "errors" - "fmt" "io" "net/http" "net/http/httptest" @@ -378,7 +377,6 @@ func Test_CallTargets(t *testing.T) { } else { assert.NoError(t, err) } - fmt.Println(respBody) assert.Equal(t, tt.res.ret, respBody) }) } diff --git a/internal/integration/client.go b/internal/integration/client.go index 55f80938b..f1fa7ca6d 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -34,11 +34,14 @@ import ( idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/object/v2" + 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" "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" + userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" "github.com/zitadel/zitadel/pkg/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" @@ -46,8 +49,7 @@ import ( settings_v2beta "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" "github.com/zitadel/zitadel/pkg/grpc/system" user_pb "github.com/zitadel/zitadel/pkg/grpc/user" - schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha" - "github.com/zitadel/zitadel/pkg/grpc/user/v2" + user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) @@ -57,7 +59,7 @@ type Client struct { Mgmt mgmt.ManagementServiceClient Auth auth.AuthServiceClient UserV2beta user_v2beta.UserServiceClient - UserV2 user.UserServiceClient + UserV2 user_v2.UserServiceClient SessionV2beta session_v2beta.SessionServiceClient SessionV2 session.SessionServiceClient SettingsV2beta settings_v2beta.SettingsServiceClient @@ -70,9 +72,10 @@ type Client struct { ActionV3Alpha action.ZITADELActionsClient FeatureV2beta feature_v2beta.FeatureServiceClient FeatureV2 feature.FeatureServiceClient - UserSchemaV3 schema.UserSchemaServiceClient + UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient IDPv2 idp_pb.IdentityProviderServiceClient + UserV3Alpha user_v3alpha.ZITADELUsersClient } func newClient(cc *grpc.ClientConn) Client { @@ -82,7 +85,7 @@ func newClient(cc *grpc.ClientConn) Client { Mgmt: mgmt.NewManagementServiceClient(cc), Auth: auth.NewAuthServiceClient(cc), UserV2beta: user_v2beta.NewUserServiceClient(cc), - UserV2: user.NewUserServiceClient(cc), + UserV2: user_v2.NewUserServiceClient(cc), SessionV2beta: session_v2beta.NewSessionServiceClient(cc), SessionV2: session.NewSessionServiceClient(cc), SettingsV2beta: settings_v2beta.NewSettingsServiceClient(cc), @@ -95,9 +98,10 @@ func newClient(cc *grpc.ClientConn) Client { ActionV3Alpha: action.NewZITADELActionsClient(cc), FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), FeatureV2: feature.NewFeatureServiceClient(cc), - UserSchemaV3: schema.NewUserSchemaServiceClient(cc), + UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc), WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc), IDPv2: idp_pb.NewIdentityProviderServiceClient(cc), + UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), } } @@ -148,29 +152,29 @@ func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx conte return primaryDomain, instanceId, adminUser.GetUserId(), t.updateInstanceAndOrg(newCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.ExternalPort)) } -func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ +func (s *Tester) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse { + resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ OrgId: s.Organisation.ID, }, }, - Profile: &user.SetHumanProfile{ + Profile: &user_v2.SetHumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", PreferredLanguage: gu.Ptr("nl"), - Gender: gu.Ptr(user.Gender_GENDER_MALE), + Gender: gu.Ptr(user_v2.Gender_GENDER_MALE), }, - Email: &user.SetHumanEmail{ + Email: &user_v2.SetHumanEmail{ Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), - Verification: &user.SetHumanEmail_ReturnCode{ - ReturnCode: &user.ReturnEmailVerificationCode{}, + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, }, }, - Phone: &user.SetHumanPhone{ + Phone: &user_v2.SetHumanPhone{ Phone: "+41791234567", - Verification: &user.SetHumanPhone_ReturnCode{ - ReturnCode: &user.ReturnPhoneVerificationCode{}, + Verification: &user_v2.SetHumanPhone_ReturnCode{ + ReturnCode: &user_v2.ReturnPhoneVerificationCode{}, }, }, }) @@ -178,23 +182,23 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse return resp } -func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ +func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse { + resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ OrgId: s.Organisation.ID, }, }, - Profile: &user.SetHumanProfile{ + Profile: &user_v2.SetHumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", PreferredLanguage: gu.Ptr("nl"), - Gender: gu.Ptr(user.Gender_GENDER_MALE), + Gender: gu.Ptr(user_v2.Gender_GENDER_MALE), }, - Email: &user.SetHumanEmail{ + Email: &user_v2.SetHumanEmail{ Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), - Verification: &user.SetHumanEmail_ReturnCode{ - ReturnCode: &user.ReturnEmailVerificationCode{}, + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, }, }, }) @@ -202,29 +206,29 @@ func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user.AddHumanUserR return resp } -func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ +func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse { + resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ OrgId: s.Organisation.ID, }, }, - Profile: &user.SetHumanProfile{ + Profile: &user_v2.SetHumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", PreferredLanguage: gu.Ptr("nl"), - Gender: gu.Ptr(user.Gender_GENDER_MALE), + Gender: gu.Ptr(user_v2.Gender_GENDER_MALE), }, - Email: &user.SetHumanEmail{ + Email: &user_v2.SetHumanEmail{ Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), - Verification: &user.SetHumanEmail_ReturnCode{ - ReturnCode: &user.ReturnEmailVerificationCode{}, + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, }, }, - Phone: &user.SetHumanPhone{ + Phone: &user_v2.SetHumanPhone{ Phone: "+41791234567", - Verification: &user.SetHumanPhone_ReturnCode{ - ReturnCode: &user.ReturnPhoneVerificationCode{}, + Verification: &user_v2.SetHumanPhone_ReturnCode{ + ReturnCode: &user_v2.ReturnPhoneVerificationCode{}, }, }, TotpSecret: gu.Ptr(secret), @@ -239,15 +243,15 @@ func (s *Tester) CreateOrganization(ctx context.Context, name, adminEmail string Admins: []*org.AddOrganizationRequest_Admin{ { UserType: &org.AddOrganizationRequest_Admin_Human{ - Human: &user.AddHumanUserRequest{ - Profile: &user.SetHumanProfile{ + Human: &user_v2.AddHumanUserRequest{ + Profile: &user_v2.SetHumanProfile{ GivenName: "firstname", FamilyName: "lastname", }, - Email: &user.SetHumanEmail{ + Email: &user_v2.SetHumanEmail{ Email: adminEmail, - Verification: &user.SetHumanEmail_ReturnCode{ - ReturnCode: &user.ReturnEmailVerificationCode{}, + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, }, }, }, @@ -292,29 +296,29 @@ func (s *Tester) CreateOrganizationWithUserID(ctx context.Context, name, userID return resp } -func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ +func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user_v2.AddHumanUserResponse { + resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ OrgId: org, }, }, - Profile: &user.SetHumanProfile{ + Profile: &user_v2.SetHumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), PreferredLanguage: gu.Ptr("nl"), - Gender: gu.Ptr(user.Gender_GENDER_MALE), + Gender: gu.Ptr(user_v2.Gender_GENDER_MALE), }, - Email: &user.SetHumanEmail{ + Email: &user_v2.SetHumanEmail{ Email: email, - Verification: &user.SetHumanEmail_IsVerified{ + Verification: &user_v2.SetHumanEmail_IsVerified{ IsVerified: true, }, }, - Phone: &user.SetHumanPhone{ + Phone: &user_v2.SetHumanPhone{ Phone: "+41791234567", - Verification: &user.SetHumanPhone_IsVerified{ + Verification: &user_v2.SetHumanPhone_IsVerified{ IsVerified: true, }, }, @@ -334,12 +338,12 @@ func (s *Tester) CreateMachineUser(ctx context.Context) *mgmt.AddMachineUserResp return resp } -func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) *user.AddIDPLinkResponse { +func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) *user_v2.AddIDPLinkResponse { resp, err := s.Client.UserV2.AddIDPLink( ctx, - &user.AddIDPLinkRequest{ + &user_v2.AddIDPLinkRequest{ UserId: userID, - IdpLink: &user.IDPLink{ + IdpLink: &user_v2.IDPLink{ IdpId: idpID, UserId: externalID, UserName: username, @@ -351,13 +355,13 @@ func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpI } func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) { - reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user.CreatePasskeyRegistrationLinkRequest{ + reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user_v2.CreatePasskeyRegistrationLinkRequest{ UserId: userID, - Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, + Medium: &user_v2.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, }) logging.OnError(err).Fatal("create user passkey") - pkr, err := s.Client.UserV2.RegisterPasskey(ctx, &user.RegisterPasskeyRequest{ + pkr, err := s.Client.UserV2.RegisterPasskey(ctx, &user_v2.RegisterPasskeyRequest{ UserId: userID, Code: reg.GetCode(), Domain: s.Config.ExternalDomain, @@ -366,7 +370,7 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) { attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) logging.OnError(err).Fatal("create user passkey") - _, err = s.Client.UserV2.VerifyPasskeyRegistration(ctx, &user.VerifyPasskeyRegistrationRequest{ + _, err = s.Client.UserV2.VerifyPasskeyRegistration(ctx, &user_v2.VerifyPasskeyRegistrationRequest{ UserId: userID, PasskeyId: pkr.GetPasskeyId(), PublicKeyCredential: attestationResponse, @@ -376,7 +380,7 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) { } func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) { - pkr, err := s.Client.UserV2.RegisterU2F(ctx, &user.RegisterU2FRequest{ + pkr, err := s.Client.UserV2.RegisterU2F(ctx, &user_v2.RegisterU2FRequest{ UserId: userID, Domain: s.Config.ExternalDomain, }) @@ -384,7 +388,7 @@ func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) { attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) logging.OnError(err).Fatal("create user u2f") - _, err = s.Client.UserV2.VerifyU2FRegistration(ctx, &user.VerifyU2FRegistrationRequest{ + _, err = s.Client.UserV2.VerifyU2FRegistration(ctx, &user_v2.VerifyU2FRegistrationRequest{ UserId: userID, U2FId: pkr.GetU2FId(), PublicKeyCredential: attestationResponse, @@ -394,9 +398,9 @@ func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) { } func (s *Tester) SetUserPassword(ctx context.Context, userID, password string, changeRequired bool) *object.Details { - resp, err := s.Client.UserV2.SetPassword(ctx, &user.SetPasswordRequest{ + resp, err := s.Client.UserV2.SetPassword(ctx, &user_v2.SetPasswordRequest{ UserId: userID, - NewPassword: &user.Password{ + NewPassword: &user_v2.Password{ Password: password, ChangeRequired: changeRequired, }, @@ -757,24 +761,57 @@ func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Co return target } -func (s *Tester) CreateUserSchema(ctx context.Context, t *testing.T) *schema.CreateUserSchemaResponse { - return s.CreateUserSchemaWithType(ctx, t, fmt.Sprint(time.Now().UnixNano()+1)) +func (s *Tester) CreateUserSchemaEmpty(ctx context.Context) *userschema_v3alpha.CreateUserSchemaResponse { + return s.CreateUserSchemaEmptyWithType(ctx, fmt.Sprint(time.Now().UnixNano()+1)) } -func (s *Tester) CreateUserSchemaWithType(ctx context.Context, t *testing.T, schemaType string) *schema.CreateUserSchemaResponse { +func (s *Tester) CreateUserSchema(ctx context.Context, schemaData []byte) *userschema_v3alpha.CreateUserSchemaResponse { + userSchema := new(structpb.Struct) + err := userSchema.UnmarshalJSON(schemaData) + logging.OnError(err).Fatal("create userschema unmarshal") + schema, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{ + UserSchema: &userschema_v3alpha.CreateUserSchema{ + Type: fmt.Sprint(time.Now().UnixNano() + 1), + DataType: &userschema_v3alpha.CreateUserSchema_Schema{ + Schema: userSchema, + }, + }, + }) + logging.OnError(err).Fatal("create userschema") + return schema +} + +func (s *Tester) CreateUserSchemaEmptyWithType(ctx context.Context, schemaType string) *userschema_v3alpha.CreateUserSchemaResponse { userSchema := new(structpb.Struct) err := userSchema.UnmarshalJSON([]byte(`{ "$schema": "urn:zitadel:schema:v1", "type": "object", "properties": {} }`)) - require.NoError(t, err) - target, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &schema.CreateUserSchemaRequest{ - Type: schemaType, - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: userSchema, + logging.OnError(err).Fatal("create userschema unmarshal") + schema, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{ + UserSchema: &userschema_v3alpha.CreateUserSchema{ + Type: schemaType, + DataType: &userschema_v3alpha.CreateUserSchema_Schema{ + Schema: userSchema, + }, }, }) - require.NoError(t, err) - return target + logging.OnError(err).Fatal("create userschema") + return schema +} + +func (s *Tester) CreateSchemaUser(ctx context.Context, orgID string, schemaID string, data []byte) *user_v3alpha.CreateUserResponse { + userData := new(structpb.Struct) + err := userData.UnmarshalJSON(data) + logging.OnError(err).Fatal("create user unmarshal") + user, err := s.Client.UserV3Alpha.CreateUser(ctx, &user_v3alpha.CreateUserRequest{ + Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}}, + User: &user_v3alpha.CreateUser{ + SchemaId: schemaID, + Data: userData, + }, + }) + logging.OnError(err).Fatal("create user") + return user } diff --git a/internal/query/projection/user_schema.go b/internal/query/projection/user_schema.go index 2961596e3..714060403 100644 --- a/internal/query/projection/user_schema.go +++ b/internal/query/projection/user_schema.go @@ -12,9 +12,10 @@ import ( ) const ( - UserSchemaTable = "projections.user_schemas" + UserSchemaTable = "projections.user_schemas1" UserSchemaIDCol = "id" + UserSchemaCreationDateCol = "creation_date" UserSchemaChangeDateCol = "change_date" UserSchemaSequenceCol = "sequence" UserSchemaInstanceIDCol = "instance_id" @@ -39,6 +40,7 @@ func (*userSchemaProjection) Init() *old_handler.Check { return handler.NewTableCheck( handler.NewTable([]*handler.InitColumn{ handler.NewColumn(UserSchemaIDCol, handler.ColumnTypeText), + handler.NewColumn(UserSchemaCreationDateCol, handler.ColumnTypeTimestamp), handler.NewColumn(UserSchemaChangeDateCol, handler.ColumnTypeTimestamp), handler.NewColumn(UserSchemaSequenceCol, handler.ColumnTypeInt64), handler.NewColumn(UserSchemaStateCol, handler.ColumnTypeEnum), @@ -102,6 +104,7 @@ func (p *userSchemaProjection) reduceCreated(event eventstore.Event) (*handler.S event, []handler.Column{ handler.NewCol(UserSchemaIDCol, event.Aggregate().ID), + handler.NewCol(UserSchemaCreationDateCol, handler.OnlySetValueOnInsert(UserSchemaTable, e.CreationDate())), handler.NewCol(UserSchemaChangeDateCol, event.CreatedAt()), handler.NewCol(UserSchemaSequenceCol, event.Sequence()), handler.NewCol(UserSchemaInstanceIDCol, event.Aggregate().InstanceID), @@ -130,7 +133,10 @@ func (p *userSchemaProjection) reduceUpdated(event eventstore.Event) (*handler.S if len(e.Schema) > 0 { cols = append(cols, handler.NewCol(UserSchemaSchemaCol, e.Schema)) - cols = append(cols, handler.NewIncrementCol(UserSchemaRevisionCol, 1)) + } + + if e.SchemaRevision != nil { + cols = append(cols, handler.NewCol(UserSchemaRevisionCol, *e.SchemaRevision)) } if len(e.PossibleAuthenticators) > 0 { diff --git a/internal/query/projection/user_schema_test.go b/internal/query/projection/user_schema_test.go index ac7b0704c..4fecc8995 100644 --- a/internal/query/projection/user_schema_test.go +++ b/internal/query/projection/user_schema_test.go @@ -39,10 +39,11 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.user_schemas (id, change_date, sequence, instance_id, state, type, revision, schema, possible_authenticators) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.user_schemas1 (id, creation_date, change_date, sequence, instance_id, state, type, revision, schema, possible_authenticators) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", anyArg{}, + anyArg{}, uint64(15), "instance-id", domain.UserSchemaStateActive, @@ -61,9 +62,9 @@ func TestUserSchemaProjection_reduces(t *testing.T) { args: args{ event: getEvent( testEvent( - schema.CreatedType, + schema.UpdatedType, schema.AggregateType, - []byte(`{"schemaType": "type", "schema": {"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}, "possibleAuthenticators": [1,2]}`), + []byte(`{"schemaType": "type", "schemaRevision": 2, "schema": {"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}, "possibleAuthenticators": [1,2]}`), ), eventstore.GenericEventMapper[schema.UpdatedEvent]), }, reduce: (&userSchemaProjection{}).reduceUpdated, @@ -73,13 +74,13 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, type, schema, revision, possible_authenticators) = ($1, $2, $3, $4, revision + $5, $6) WHERE (id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.user_schemas1 SET (change_date, sequence, type, schema, revision, possible_authenticators) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ anyArg{}, uint64(15), "type", json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`), - 1, + uint64(2), []domain.AuthenticatorType{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, "agg-id", "instance-id", @@ -106,7 +107,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.user_schemas1 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -136,7 +137,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.user_schemas1 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -166,7 +167,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_schemas WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.user_schemas1 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -193,7 +194,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_schemas WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.user_schemas1 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/user_schema.go b/internal/query/user_schema.go index ac5dbb1d1..ff5117d26 100644 --- a/internal/query/user_schema.go +++ b/internal/query/user_schema.go @@ -26,7 +26,6 @@ func (e *UserSchemas) SetState(s *State) { } type UserSchema struct { - ID string domain.ObjectDetails State domain.UserSchemaState Type string @@ -49,6 +48,10 @@ var ( name: projection.UserSchemaIDCol, table: userSchemaTable, } + UserSchemaCreationDateCol = Column{ + name: projection.UserSchemaCreationDateCol, + table: userSchemaTable, + } UserSchemaChangeDateCol = Column{ name: projection.UserSchemaChangeDateCol, table: userSchemaTable, @@ -131,6 +134,7 @@ func NewUserSchemaStateSearchQuery(value domain.UserSchemaState) (SearchQuery, e func prepareUserSchemaQuery() (sq.SelectBuilder, func(*sql.Row) (*UserSchema, error)) { return sq.Select( UserSchemaIDCol.identifier(), + UserSchemaCreationDateCol.identifier(), UserSchemaChangeDateCol.identifier(), UserSchemaSequenceCol.identifier(), UserSchemaInstanceIDCol.identifier(), @@ -147,6 +151,7 @@ func prepareUserSchemaQuery() (sq.SelectBuilder, func(*sql.Row) (*UserSchema, er var schema database.ByteArray[byte] err := row.Scan( &u.ID, + &u.CreationDate, &u.EventDate, &u.Sequence, &u.ResourceOwner, @@ -173,6 +178,7 @@ func prepareUserSchemaQuery() (sq.SelectBuilder, func(*sql.Row) (*UserSchema, er func prepareUserSchemasQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserSchemas, error)) { return sq.Select( UserSchemaIDCol.identifier(), + UserSchemaCreationDateCol.identifier(), UserSchemaChangeDateCol.identifier(), UserSchemaSequenceCol.identifier(), UserSchemaInstanceIDCol.identifier(), @@ -195,6 +201,7 @@ func prepareUserSchemasQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserSchemas, u := new(UserSchema) err := rows.Scan( &u.ID, + &u.CreationDate, &u.EventDate, &u.Sequence, &u.ResourceOwner, diff --git a/internal/query/user_schema_test.go b/internal/query/user_schema_test.go index b9cd81b99..19526373a 100644 --- a/internal/query/user_schema_test.go +++ b/internal/query/user_schema_test.go @@ -15,19 +15,21 @@ import ( ) var ( - prepareUserSchemasStmt = `SELECT projections.user_schemas.id,` + - ` projections.user_schemas.change_date,` + - ` projections.user_schemas.sequence,` + - ` projections.user_schemas.instance_id,` + - ` projections.user_schemas.state,` + - ` projections.user_schemas.type,` + - ` projections.user_schemas.revision,` + - ` projections.user_schemas.schema,` + - ` projections.user_schemas.possible_authenticators,` + + prepareUserSchemasStmt = `SELECT projections.user_schemas1.id,` + + ` projections.user_schemas1.creation_date,` + + ` projections.user_schemas1.change_date,` + + ` projections.user_schemas1.sequence,` + + ` projections.user_schemas1.instance_id,` + + ` projections.user_schemas1.state,` + + ` projections.user_schemas1.type,` + + ` projections.user_schemas1.revision,` + + ` projections.user_schemas1.schema,` + + ` projections.user_schemas1.possible_authenticators,` + ` COUNT(*) OVER ()` + ` FROM projections.user_schemas` prepareUserSchemasCols = []string{ "id", + "creation_date", "change_date", "sequence", "instance_id", @@ -39,18 +41,20 @@ var ( "count", } - prepareUserSchemaStmt = `SELECT projections.user_schemas.id,` + - ` projections.user_schemas.change_date,` + - ` projections.user_schemas.sequence,` + - ` projections.user_schemas.instance_id,` + - ` projections.user_schemas.state,` + - ` projections.user_schemas.type,` + - ` projections.user_schemas.revision,` + - ` projections.user_schemas.schema,` + - ` projections.user_schemas.possible_authenticators` + + prepareUserSchemaStmt = `SELECT projections.user_schemas1.id,` + + ` projections.user_schemas1.creation_date,` + + ` projections.user_schemas1.change_date,` + + ` projections.user_schemas1.sequence,` + + ` projections.user_schemas1.instance_id,` + + ` projections.user_schemas1.state,` + + ` projections.user_schemas1.type,` + + ` projections.user_schemas1.revision,` + + ` projections.user_schemas1.schema,` + + ` projections.user_schemas1.possible_authenticators` + ` FROM projections.user_schemas` prepareUserSchemaCols = []string{ "id", + "creation_date", "change_date", "sequence", "instance_id", @@ -96,6 +100,7 @@ func Test_UserSchemaPrepares(t *testing.T) { { "id", testNow, + testNow, uint64(20211109), "instance-id", domain.UserSchemaStateActive, @@ -113,9 +118,10 @@ func Test_UserSchemaPrepares(t *testing.T) { }, UserSchemas: []*UserSchema{ { - ID: "id", ObjectDetails: domain.ObjectDetails{ + ID: "id", EventDate: testNow, + CreationDate: testNow, Sequence: 20211109, ResourceOwner: "instance-id", }, @@ -139,6 +145,7 @@ func Test_UserSchemaPrepares(t *testing.T) { { "id-1", testNow, + testNow, uint64(20211109), "instance-id", domain.UserSchemaStateActive, @@ -150,6 +157,7 @@ func Test_UserSchemaPrepares(t *testing.T) { { "id-2", testNow, + testNow, uint64(20211110), "instance-id", domain.UserSchemaStateInactive, @@ -167,9 +175,10 @@ func Test_UserSchemaPrepares(t *testing.T) { }, UserSchemas: []*UserSchema{ { - ID: "id-1", ObjectDetails: domain.ObjectDetails{ + ID: "id-1", EventDate: testNow, + CreationDate: testNow, Sequence: 20211109, ResourceOwner: "instance-id", }, @@ -180,9 +189,10 @@ func Test_UserSchemaPrepares(t *testing.T) { PossibleAuthenticators: database.NumberArray[domain.AuthenticatorType]{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, }, { - ID: "id-2", ObjectDetails: domain.ObjectDetails{ + ID: "id-2", EventDate: testNow, + CreationDate: testNow, Sequence: 20211110, ResourceOwner: "instance-id", }, @@ -240,6 +250,7 @@ func Test_UserSchemaPrepares(t *testing.T) { []driver.Value{ "id", testNow, + testNow, uint64(20211109), "instance-id", domain.UserSchemaStateActive, @@ -251,9 +262,10 @@ func Test_UserSchemaPrepares(t *testing.T) { ), }, object: &UserSchema{ - ID: "id", ObjectDetails: domain.ObjectDetails{ + ID: "id", EventDate: testNow, + CreationDate: testNow, Sequence: 20211109, ResourceOwner: "instance-id", }, diff --git a/internal/repository/user/schema/schema.go b/internal/repository/user/schema/schema.go index b626ee4c7..729b1aed4 100644 --- a/internal/repository/user/schema/schema.go +++ b/internal/repository/user/schema/schema.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -79,7 +81,9 @@ type UpdatedEvent struct { SchemaType *string `json:"schemaType,omitempty"` Schema json.RawMessage `json:"schema,omitempty"` PossibleAuthenticators []domain.AuthenticatorType `json:"possibleAuthenticators,omitempty"` + SchemaRevision *uint64 `json:"schemaRevision,omitempty"` oldSchemaType string + oldRevision uint64 } func (e *UpdatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { @@ -139,6 +143,13 @@ func ChangePossibleAuthenticators(possibleAuthenticators []domain.AuthenticatorT } } +func IncreaseRevision(oldRevision uint64) func(event *UpdatedEvent) { + return func(e *UpdatedEvent) { + e.SchemaRevision = gu.Ptr(oldRevision + 1) + e.oldRevision = oldRevision + } +} + type DeactivatedEvent struct { *eventstore.BaseEvent `json:"-"` } diff --git a/internal/repository/user/schemauser/aggregate.go b/internal/repository/user/schemauser/aggregate.go new file mode 100644 index 000000000..1c9901c08 --- /dev/null +++ b/internal/repository/user/schemauser/aggregate.go @@ -0,0 +1,25 @@ +package schemauser + +import ( + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + AggregateType = "user" + AggregateVersion = "v3" +) + +type Aggregate struct { + eventstore.Aggregate +} + +func NewAggregate(id, resourceOwner string) *Aggregate { + return &Aggregate{ + Aggregate: eventstore.Aggregate{ + Type: AggregateType, + Version: AggregateVersion, + ID: id, + ResourceOwner: resourceOwner, + }, + } +} diff --git a/internal/repository/user/schemauser/email.go b/internal/repository/user/schemauser/email.go new file mode 100644 index 000000000..07ae1bdf7 --- /dev/null +++ b/internal/repository/user/schemauser/email.go @@ -0,0 +1,202 @@ +package schemauser + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/internal/api/http" + "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 ( + emailEventPrefix = eventPrefix + "email." + EmailUpdatedType = emailEventPrefix + "updated" + EmailVerifiedType = emailEventPrefix + "verified" + EmailVerificationFailedType = emailEventPrefix + "verification.failed" + EmailCodeAddedType = emailEventPrefix + "code.added" + EmailCodeSentType = emailEventPrefix + "code.sent" +) + +type EmailUpdatedEvent struct { + eventstore.BaseEvent `json:"-"` + + EmailAddress domain.EmailAddress `json:"email,omitempty"` +} + +func (e *EmailUpdatedEvent) Payload() interface{} { + return e +} + +func (e *EmailUpdatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewEmailUpdatedEvent(ctx context.Context, aggregate *eventstore.Aggregate, emailAddress domain.EmailAddress) *EmailUpdatedEvent { + return &EmailUpdatedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + EmailUpdatedType, + ), + EmailAddress: emailAddress, + } +} + +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:"-"` + + IsEmailVerified bool `json:"-"` +} + +func (e *EmailVerifiedEvent) Payload() interface{} { + return nil +} + +func (e *EmailVerifiedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewEmailVerifiedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailVerifiedEvent { + return &EmailVerifiedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + EmailVerifiedType, + ), + } +} + +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:"-"` +} + +func (e *EmailVerificationFailedEvent) Payload() interface{} { + return nil +} + +func (e *EmailVerificationFailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewHumanEmailVerificationFailedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailVerificationFailedEvent { + return &EmailVerificationFailedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + EmailVerificationFailedType, + ), + } +} + +func EmailVerificationFailedEventMapper(event eventstore.Event) (eventstore.Event, error) { + return &EmailVerificationFailedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + }, nil +} + +type EmailCodeAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + Code *crypto.CryptoValue `json:"code,omitempty"` + Expiry time.Duration `json:"expiry,omitempty"` + URLTemplate string `json:"url_template,omitempty"` + CodeReturned bool `json:"code_returned,omitempty"` + TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` +} + +func (e *EmailCodeAddedEvent) Payload() interface{} { + return e +} + +func (e *EmailCodeAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *EmailCodeAddedEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + +func NewEmailCodeAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + code *crypto.CryptoValue, + expiry time.Duration, + urlTemplate string, + codeReturned bool, +) *EmailCodeAddedEvent { + return &EmailCodeAddedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + EmailCodeAddedType, + ), + Code: code, + Expiry: expiry, + URLTemplate: urlTemplate, + CodeReturned: codeReturned, + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), + } +} + +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:"-"` +} + +func (e *EmailCodeSentEvent) Payload() interface{} { + return nil +} + +func (e *EmailCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewHumanEmailCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailCodeSentEvent { + return &EmailCodeSentEvent{ + 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 new file mode 100644 index 000000000..b9cf03e5d --- /dev/null +++ b/internal/repository/user/schemauser/eventstore.go @@ -0,0 +1,9 @@ +package schemauser + +import "github.com/zitadel/zitadel/internal/eventstore" + +func init() { + eventstore.RegisterFilterEventMapper(AggregateType, CreatedType, eventstore.GenericEventMapper[CreatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, UpdatedType, eventstore.GenericEventMapper[UpdatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, DeletedType, eventstore.GenericEventMapper[DeletedEvent]) +} diff --git a/internal/repository/user/schemauser/phone.go b/internal/repository/user/schemauser/phone.go new file mode 100644 index 000000000..5110772c0 --- /dev/null +++ b/internal/repository/user/schemauser/phone.go @@ -0,0 +1,198 @@ +package schemauser + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/internal/api/http" + "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 ( + phoneEventPrefix = eventPrefix + "phone." + PhoneUpdatedType = phoneEventPrefix + "updated" + PhoneVerifiedType = phoneEventPrefix + "verified" + PhoneVerificationFailedType = phoneEventPrefix + "verification.failed" + PhoneCodeAddedType = phoneEventPrefix + "code.added" + PhoneCodeSentType = phoneEventPrefix + "code.sent" +) + +type PhoneChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + PhoneNumber domain.PhoneNumber `json:"phone,omitempty"` +} + +func (e *PhoneChangedEvent) Payload() interface{} { + return e +} + +func (e *PhoneChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewPhoneChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, phone domain.PhoneNumber) *PhoneChangedEvent { + return &PhoneChangedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + PhoneUpdatedType, + ), + PhoneNumber: phone, + } +} + +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:"-"` + + IsPhoneVerified bool `json:"-"` +} + +func (e *PhoneVerifiedEvent) Payload() interface{} { + return nil +} + +func (e *PhoneVerifiedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewPhoneVerifiedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneVerifiedEvent { + return &PhoneVerifiedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + PhoneVerifiedType, + ), + } +} + +func PhoneVerifiedEventMapper(event eventstore.Event) (eventstore.Event, error) { + return &PhoneVerifiedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + IsPhoneVerified: true, + }, nil +} + +type PhoneVerificationFailedEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *PhoneVerificationFailedEvent) Payload() interface{} { + return nil +} + +func (e *PhoneVerificationFailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewPhoneVerificationFailedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneVerificationFailedEvent { + return &PhoneVerificationFailedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + PhoneVerificationFailedType, + ), + } +} + +func PhoneVerificationFailedEventMapper(event eventstore.Event) (eventstore.Event, error) { + return &PhoneVerificationFailedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + }, nil +} + +type PhoneCodeAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + Code *crypto.CryptoValue `json:"code,omitempty"` + Expiry time.Duration `json:"expiry,omitempty"` + CodeReturned bool `json:"code_returned,omitempty"` + TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` +} + +func (e *PhoneCodeAddedEvent) Payload() interface{} { + return e +} + +func (e *PhoneCodeAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *PhoneCodeAddedEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + +func NewPhoneCodeAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + code *crypto.CryptoValue, + expiry time.Duration, + codeReturned bool, +) *PhoneCodeAddedEvent { + return &PhoneCodeAddedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + PhoneCodeAddedType, + ), + Code: code, + Expiry: expiry, + CodeReturned: codeReturned, + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), + } +} + +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:"-"` +} + +func (e *PhoneCodeSentEvent) Payload() interface{} { + return e +} + +func (e *PhoneCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewPhoneCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneCodeSentEvent { + return &PhoneCodeSentEvent{ + 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 new file mode 100644 index 000000000..4c88d5308 --- /dev/null +++ b/internal/repository/user/schemauser/user.go @@ -0,0 +1,144 @@ +package schemauser + +import ( + "context" + "encoding/json" + + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + eventPrefix = "user." + CreatedType = eventPrefix + "created" + UpdatedType = eventPrefix + "updated" + DeletedType = eventPrefix + "deleted" +) + +type CreatedEvent struct { + *eventstore.BaseEvent `json:"-"` + ID string `json:"id"` + SchemaID string `json:"schemaID"` + SchemaRevision uint64 `json:"schemaRevision"` + Data json.RawMessage `json:"user,omitempty"` +} + +func (e *CreatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *CreatedEvent) Payload() interface{} { + return e +} + +func (e *CreatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewCreatedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + + schemaID string, + schemaRevision uint64, + data json.RawMessage, +) *CreatedEvent { + return &CreatedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + CreatedType, + ), + SchemaID: schemaID, + SchemaRevision: schemaRevision, + Data: data, + } +} + +type UpdatedEvent struct { + *eventstore.BaseEvent `json:"-"` + + 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) { + e.BaseEvent = event +} + +func (e *UpdatedEvent) Payload() interface{} { + return e +} + +func (e *UpdatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} +func NewUpdatedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + changes []Changes, +) *UpdatedEvent { + updatedEvent := &UpdatedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + UpdatedType, + ), + } + for _, change := range changes { + change(updatedEvent) + } + return updatedEvent +} + +type Changes func(event *UpdatedEvent) + +func ChangeSchemaID(oldSchemaID, schemaID string) func(event *UpdatedEvent) { + return func(e *UpdatedEvent) { + e.SchemaID = &schemaID + e.oldSchemaID = oldSchemaID + } +} +func ChangeSchemaRevision(oldSchemaRevision, schemaRevision uint64) func(event *UpdatedEvent) { + return func(e *UpdatedEvent) { + e.SchemaRevision = &schemaRevision + e.oldRevision = oldSchemaRevision + } +} + +func ChangeData(data json.RawMessage) func(event *UpdatedEvent) { + return func(e *UpdatedEvent) { + e.Data = data + } +} + +type DeletedEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *DeletedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *DeletedEvent) Payload() interface{} { + return e +} + +func (e *DeletedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewDeletedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *DeletedEvent { + return &DeletedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + DeletedType, + ), + } +} diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 291cfb044..f89afe844 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -593,6 +593,11 @@ Errors: NotActive: Потребителската схема не е активна NotInactive: Потребителската схема не е неактивна NotExists: Потребителската схема не съществува + ID: + Missing: Липсва идентификатор на потребителска схема + Invalid: Потребителската схема е невалидна + Data: + Invalid: Невалидни данни за потребителска схема TokenExchange: FeatureDisabled: Функцията Token Exchange е деактивирана за вашето копие. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1351,6 +1356,26 @@ EventTypes: deactivated: Потребителската схема е деактивирана reactivated: Потребителската схема е активирана отново deleted: Потребителската схема е изтрита + user: + created: Потребител е създаден + updated: Потребителят е актуализиран + deleted: Потребителят е изтрит + email: + updated: Имейл адресът е променен + verified: Имейл адресът е потвърден + verification: + failed: Проверката на имейл адреса не бе успешна + code: + added: Генериран код за потвърждение на имейл адрес + sent: Кодът за потвърждение на имейл адреса е изпратен + phone: + updated: Телефонният номер е променен + verified: Телефонният номер е потвърден + verification: + failed: Неуспешна проверка на телефонния номер + code: + added: Генериран код за потвърждение на телефонен номер + sent: Кодът за потвърждение на телефонния номер е изпратен web_key: added: Добавен уеб ключ activated: Уеб ключът е активиран diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 17e15f91d..1d7afec91 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -574,6 +574,11 @@ Errors: NotActive: Uživatelské schéma není aktivní NotInactive: Uživatelské schéma není neaktivní NotExists: Uživatelské schéma neexistuje + ID: + Missing: Chybí ID schématu uživatele + Invalid: Uživatelské schéma je neplatné + Data: + Invalid: Data neplatná pro uživatelské schéma TokenExchange: FeatureDisabled: Funkce Token Exchange je pro vaši instanci zakázána. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1317,6 +1322,26 @@ EventTypes: deactivated: Uživatelské schéma deaktivováno reactivated: Uživatelské schéma bylo znovu aktivováno deleted: Uživatelské schéma bylo smazáno + user: + created: Uživatel vytvořen + updated: Uživatel aktualizován + deleted: Uživatel byl smazán + email: + updated: E-mailová adresa změněna + verified: E-mailová adresa ověřena + verification: + failed: Ověření e-mailové adresy se nezdařilo + code: + added: Vygenerován ověřovací kód e-mailové adresy + sent: Ověřovací kód e-mailové adresy odeslán + phone: + updated: Telefonní číslo změněno + verified: Telefonní číslo ověřeno + verification: + failed: Ověření telefonního čísla se nezdařilo + code: + added: Byl vygenerován ověřovací kód telefonního čísla + sent: Ověřovací kód telefonního čísla odeslán web_key: added: Přidán webový klíč activated: Web Key aktivován diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index de1ad3b0a..982693a49 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -576,6 +576,11 @@ Errors: NotActive: Benutzerschema nicht aktiv NotInactive: Benutzerschema nicht inaktiv NotExists: Benutzerschema existiert nicht + ID: + Missing: BenutzerschemaID fehlt + Invalid: Benutzerschema ist ungültig + Data: + Invalid: Daten für Benutzerschema ungültig TokenExchange: FeatureDisabled: Die Token-Austauschfunktion ist für Ihre Instanz deaktiviert. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1319,6 +1324,26 @@ EventTypes: deactivated: Benutzerschema deaktiviert reactivated: Benutzerschema reaktiviert deleted: Benutzerschema gelöscht + user: + created: Benutzer erstellt + updated: Benutzer aktualisiert + deleted: Benutzer gelöscht + email: + updated: E-Mail-Adresse geändert + verified: E-Mail-Adresse verifiziert + verification: + failed: E-Mail-Adressenverifizierung fehlgeschlagen + code: + added: E-Mail-Adressenverifizierungscode generiert + sent: E-Mail-Adressenverifizierungscode gesendet + phone: + updated: Telefonnummer geändert + verified: Telefonnummer verifiziert + verification: + failed: Telefonnummernverifizierung fehlgeschlagen + code: + added: Telefonnummernverifizierungscode generiert + sent: Telefonnummernverifizierungscode gesendet web_key: added: Web Key hinzugefügt activated: Web Key aktiviert diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 5d13ac234..ea15bd8a2 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -576,6 +576,11 @@ Errors: NotActive: User Schema not active NotInactive: User Schema not inactive NotExists: User Schema does not exist + ID: + Missing: User Schema ID missing + Invalid: User Schema invalid + Data: + Invalid: Data invalid for User Schema TokenExchange: FeatureDisabled: Token Exchange feature is disabled for your instance. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1319,6 +1324,28 @@ EventTypes: deactivated: User Schema deactivated reactivated: User Schema reactivated deleted: User Schema deleted + user: + created: User created + updated: User updated + deleted: User deleted + email: + updated: Email address changed + verified: Email address verified + verification: + failed: Email address verification failed + code: + added: Email address verification code generated + sent: Email address verification code sent + phone: + updated: Phone number changed + verified: Phone number verified + verification: + failed: Phone number verification failed + code: + added: Phone number verification code generated + sent: Phone number verification code sent + + web_key: added: Web Key added activated: Web Key activated diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index cbd75d259..dca302f73 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -576,6 +576,11 @@ Errors: NotActive: Esquema de usuario no activo NotInactive: Esquema de usuario no inactivo NotExists: El esquema de usuario no existe + ID: + Missing: Falta el ID del esquema de usuario + Invalid: Esquema de usuario no válido + Data: + Invalid: Datos no válidos para el esquema de usuario TokenExchange: FeatureDisabled: La función de intercambio de tokens está deshabilitada para su instancia. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1319,6 +1324,26 @@ EventTypes: deactivated: Esquema de usuario desactivado reactivated: Esquema de usuario reactivado deleted: Esquema de usuario eliminado + user: + created: Usuario creado + updated: Usuario actualizado + deleted: Usuario eliminado + email: + updated: Dirección de correo electrónico modificada + verified: Dirección de correo electrónico verificada + verification: + failed: Error en la verificación de la dirección de correo electrónico + code: + added: Código de verificación de la dirección de correo electrónico generado + sent: Código de verificación de la dirección de correo electrónico enviado + phone: + updated: Número de teléfono modificado + verified: Número de teléfono verificado + verification: + failed: Error en la verificación del número de teléfono + code: + added: Código de verificación del número de teléfono generado + sent: Código de verificación del número de teléfono enviado web_key: added: Clave web añadida activated: Clave web activada diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 1a17c3cfb..8d6e653e7 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -576,6 +576,11 @@ Errors: NotActive: Schéma utilisateur non actif NotInactive: Le schéma utilisateur n'est pas inactif NotExists: Le schéma utilisateur n'existe pas + ID: + Missing: ID de schéma utilisateur manquant + Invalid: Schéma utilisateur non valide + Data: + Invalid: Données non valides pour le schéma utilisateur TokenExchange: FeatureDisabled: La fonctionnalité Token Exchange est désactivée pour votre instance. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1314,6 +1319,26 @@ EventTypes: deactivated: Schéma utilisateur désactivé reactivated: Schéma utilisateur réactivé deleted: Schéma utilisateur supprimé + user: + created: Utilisateur créé + updated: Utilisateur mis à jour + deleted: Utilisateur supprimé + email: + updated: Adresse e-mail modifiée + verified: Adresse e-mail vérifiée + verification: + failed: Échec de la vérification de l'adresse e-mail + code: + added: Code de vérification de l'adresse e-mail généré + sent: Code de vérification de l'adresse e-mail envoyé + phone: + updated: Numéro de téléphone modifié + verified: Numéro de téléphone vérifié + verification: + failed: Échec de la vérification du numéro de téléphone + code: + added: Code de vérification du numéro de téléphone généré + sent: Code de vérification du numéro de téléphone envoyé web_key: added: Clé Web ajoutée activated: Clé Web activée diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index a8d80780e..b6c33023c 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -576,6 +576,11 @@ Errors: NotActive: Schema utente non attivo NotInactive: Schema utente non inattivo NotExists: Lo schema utente non esiste + ID: + Missing: ID schema utente mancante + Invalid: Schema utente non valido + Data: + Invalid: Dati non validi per lo schema utente TokenExchange: FeatureDisabled: La funzionalità di scambio token è disabilitata per la tua istanza. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1315,6 +1320,26 @@ EventTypes: deactivated: Schema utente disattivato reactivated: Schema utente riattivato deleted: Schema utente eliminato + user: + created: Utente creato + updated: Utente aggiornato + deleted: Utente eliminato + email: + updated: Indirizzo email modificato + verified: Indirizzo email verificato + verification: + failed: Verifica indirizzo email non riuscita + code: + added: Codice di verifica indirizzo email generato + sent: Codice di verifica indirizzo email inviato + phone: + updated: Numero di telefono modificato + verified: Numero di telefono verificato + verification: + failed: Verifica numero di telefono non riuscita + code: + added: Codice di verifica numero di telefono generato + sent: Codice di verifica numero di telefono inviato web_key: added: Web Key aggiunto activated: Web Key attivato diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 13352e6c3..df23a56f5 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -565,6 +565,11 @@ Errors: NotActive: ユーザースキーマがアクティブではありません NotInactive: ユーザースキーマが非アクティブではありません NotExists: ユーザースキーマが存在しません + ID: + Missing: ユーザー スキーマ ID がありません + Invalid: ユーザー スキーマが無効です + Data: + Invalid: ユーザー スキーマのデータが無効です TokenExchange: FeatureDisabled: インスタンスではトークン交換機能が無効になっています。 https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1305,6 +1310,26 @@ EventTypes: deactivated: ユーザースキーマが非アクティブ化されました reactivated: ユーザースキーマが再アクティブ化されました deleted: ユーザースキーマが削除されました + user: + created: ユーザーが作成されました + updated: ユーザーが更新されました + deleted: ユーザーが削除されました + email: + updated: メールアドレスが変更されました + verified: メールアドレスが確認されました + verification: + failed: メールアドレスの確認に失敗しました + code: + added: メールアドレスの確認コードが生成されました + sent: メールアドレスの確認コードが送信されました + phone: + updated: 電話番号が変更されました + verified: 電話番号が確認されました + verification: + failed: 電話番号の確認に失敗しました + code: + added: 電話番号の確認コードが生成されました + sent: 電話番号の確認コードが送信されました web_key: added: Web キーが追加されました activated: Web キーが有効化されました diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 9abd478fa..cc27d3fb3 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -575,6 +575,11 @@ Errors: NotActive: Корисничката шема не е активна NotInactive: Корисничката шема не е неактивна NotExists: Корисничката шема не постои + ID: + Missing: Недостасува ID на корисничка шема + Invalid: Корисничката шема е неважечка + Data: + Invalid: Податоците не се валидни за корисничка шема TokenExchange: FeatureDisabled: Функцијата за размена на токени е оневозможена на вашиот пример. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1317,6 +1322,26 @@ EventTypes: deactivated: Корисничката шема е деактивирана reactivated: Корисничката шема е реактивирана deleted: Корисничката шема е избришана + user: + created: Корисникот е создаден + updated: Корисникот е ажуриран + deleted: Корисникот е избришан + email: + updated: Адресата на е-пошта е променета + verified: Адресата на е-пошта е потврдена + verification: + failed: Потврдата на адресата на е-пошта не успеа + code: + added: Генериран е код за потврда на адресата на е-пошта + sent: Испратен е код за потврда на адресата на е-пошта + phone: + updated: Телефонскиот број е променет + verified: Телефонскиот број е потврден + verification: + failed: Потврдата на телефонскиот број не успеа + code: + added: Генериран е кодот за потврда на телефонскиот број + sent: Кодот за потврда на телефонскиот број е испратен web_key: added: Додаден е веб-клуч activated: Веб-клучот е активиран diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 3e13c1bf7..6d8b8014f 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -576,6 +576,11 @@ Errors: NotActive: Gebruikersschema niet actief NotInactive: Gebruikersschema niet inactief NotExists: Gebruikersschema bestaat niet + ID: + Missing: Недостасува ID на корисничка шема + Invalid: Корисничката шема е неважечка + Data: + Invalid: Податоците не се валидни за корисничка шема TokenExchange: FeatureDisabled: De Token Exchange-functie is uitgeschakeld voor uw instantie. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1314,6 +1319,26 @@ EventTypes: deactivated: Gebruikersschema gedeactiveerd reactivated: Gebruikersschema opnieuw geactiveerd deleted: Gebruikersschema verwijderd + user: + created: Gebruiker aangemaakt + updated: Gebruiker bijgewerkt + deleted: Gebruiker verwijderd + email: + updated: E-mailadres gewijzigd + verified: E-mailadres geverifieerd + verification: + failed: E-mailadres verificatie mislukt + code: + added: E-mailadres verificatiecode gegenereerd + sent: E-mailadres verificatiecode verzonden + phone: + updated: Telefoonnummer gewijzigd + verified: Telefoonnummer geverifieerd + verification: + failed: Telefoonnummer verificatie mislukt + code: + added: Telefoonnummer verificatiecode gegenereerd + sent: Telefoonnummer verificatiecode verzonden web_key: added: Web Key toegevoegd activated: Web Key geactiveerd diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 90b31e486..9508fb801 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -576,6 +576,11 @@ Errors: NotActive: Schemat użytkownika nieaktywny NotInactive: Schemat użytkownika nie jest nieaktywny NotExists: Schemat użytkownika nie istnieje + ID: + Missing: Brak identyfikatora schematu użytkownika + Invalid: Nieprawidłowy schemat użytkownika + Data: + Invalid: Nieprawidłowe dane dla schematu użytkownika TokenExchange: FeatureDisabled: Funkcja wymiany tokenów jest wyłączona dla Twojej instancji. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1319,6 +1324,26 @@ EventTypes: deactivated: Schemat użytkownika dezaktywowany reactivated: Schemat użytkownika został ponownie aktywowany deleted: Schemat użytkownika został usunięty + user: + created: Użytkownik utworzony + updated: Użytkownik zaktualizowany + deleted: Użytkownik usunięty + email: + updated: Adres e-mail zmieniony + verified: Adres e-mail zweryfikowany + verification: + failed: Weryfikacja adresu e-mail nie powiodła się + code: + added: Wygenerowano kod weryfikacyjny adresu e-mail + sent: Wysłano kod weryfikacyjny adresu e-mail + phone: + updated: Numer telefonu zmieniony + verified: Numer telefonu zweryfikowany + verification: + failed: Weryfikacja numeru telefonu nie powiodła się + code: + added: Wygenerowano kod weryfikacyjny numeru telefonu + sent: Wysłano kod weryfikacyjny numeru telefonu web_key: added: Dodano klucz internetowy activated: Klucz internetowy aktywowano diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index dd7cb4a86..aa1a809f5 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -571,6 +571,11 @@ Errors: NotActive: Esquema do usuário não ativo NotInactive: Esquema do usuário não inativo NotExists: O esquema do usuário não existe + ID: + Missing: ID do esquema do utilizador em falta + Invalid: Esquema de utilizador inválido + Data: + Invalid: Dados inválidos para o esquema do utilizador TokenExchange: FeatureDisabled: O recurso Token Exchange está desabilitado para sua instância. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1311,6 +1316,26 @@ EventTypes: deactivated: Esquema de usuário desativado reactivated: Esquema do usuário reativado deleted: Esquema do usuário excluído + user: + created: Utilizador criado + updated: Utilizador atualizado + deleted: Utilizador excluído + email: + updated: Endereço de e-mail alterado + verified: Endereço de e-mail verificado + verification: + failed: Falha na verificação do endereço de e-mail + code: + added: Código de verificação do endereço de e-mail gerado + sent: Código de verificação do endereço de e-mail enviado + phone: + updated: Número de telefone alterado + verified: Número de telefone verificado + verification: + failed: Falha na verificação do número de telefone + code: + added: Código de verificação do número de telefone gerado + sent: Código de verificação do número de telefone enviado web_key: added: Chave Web adicionada activated: Chave Web ativada diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 8b992dba0..eb846a687 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -565,6 +565,11 @@ Errors: NotActive: Пользовательская схема не активна NotInactive: Пользовательская схема не неактивна NotExists: Пользовательская схема не существует + ID: + Missing: Отсутствует идентификатор схемы пользователя + Invalid: Недействительная схема пользователя + Data: + Invalid: Данные недействительны для схемы пользователя TokenExchange: FeatureDisabled: Функция обмена токенами отключена для вашего экземпляра. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1305,6 +1310,26 @@ EventTypes: deactivated: Пользовательская схема деактивирована reactivated: Пользовательская схема повторно активирована deleted: Пользовательская схема удалена + user: + created: Пользователь создан + updated: Пользователь обновлен + deleted: Пользователь удален + email: + updated: Адрес электронной почты изменен + verified: Адрес электронной почты проверен + verification: + failed: Проверка адреса электронной почты не удалась + code: + added: Сгенерирован код проверки адреса электронной почты + sent: Отправлен код проверки адреса электронной почты + phone: + updated: Номер телефона изменен + verified: Номер телефона проверен + verification: + failed: Проверка номера телефона не удалась + code: + added: Сгенерирован код проверки номера телефона + sent: Отправлен код проверки номера телефона web_key: added: Добавлен веб-ключ activated: Веб-ключ активирован diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index f6013bd18..da33e4226 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -576,6 +576,11 @@ Errors: NotActive: Användarschema inte aktivt NotInactive: Användarschema inte inaktivt NotExists: Användarschema existerar inte + ID: + Missing: Användarschema-ID saknas + Invalid: Ogiltigt användarschema + Data: + Invalid: Data ogiltig för användarschema TokenExchange: FeatureDisabled: Token Exchange-funktionen är inaktiverad för din instans. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1319,6 +1324,26 @@ EventTypes: deactivated: Användarschema avaktiverat reactivated: Användarschema återaktiverat deleted: Användarschema borttaget + user: + created: Användare skapad + updated: Användaren uppdaterad + deleted: Användare raderad + email: + updated: E-postadress ändrad + verified: E-postadress verifierad + verification: + failed: E-postadressverifiering misslyckades + code: + added: Verifieringskod för e-postadress genererad + sent: E-postadressens verifieringskod har skickats + phone: + updated: Telefonnummer ändrat + verified: Telefonnummer verifierat + verification: + failed: Verifiering av telefonnummer misslyckades + code: + added: Verifieringskod för telefonnummer genererad + sent: Verifieringskoden för telefonnummer har skickats web_key: added: Webbnyckel har lagts till activated: Webbnyckel aktiverad diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 79f06555f..774232ea9 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -576,6 +576,11 @@ Errors: NotActive: 用户架构未激活 NotInactive: 用户架构未处于非活动状态 NotExists: 用户架构不存在 + ID: + Missing: 缺少用户架构 ID + Invalid: 用户架构无效 + Data: + Invalid: 用户架构的数据无效 TokenExchange: FeatureDisabled: 您的实例已禁用令牌交换功能。 https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1318,6 +1323,26 @@ EventTypes: deactivated: 用户架构已停用 reactivated: 用户架构已重新激活 deleted: 用户架构已删除 + user: + created: 用户已创建 + updated: 用户已更新 + deleted: 用户已删除 + email: + updated: 电子邮件地址已更改 + verified: 电子邮件地址已验证 + verification: + failed: 电子邮件地址验证失败 + code: + added: 电子邮件地址验证码已生成 + sent: 电子邮件地址验证码已发送 + phone: + updated: 电话号码已更改 + verified: 电话号码已验证 + verification: + failed: 电话号码验证失败 + code: + added: 电话号码验证码已生成 + sent: 电话号码验证码已发送 web_key: added: 已添加 Web Key activated: 已激活 Web Key diff --git a/proto/zitadel/object/v3alpha/object.proto b/proto/zitadel/object/v3alpha/object.proto index 5c067ddc1..a3a2d2325 100644 --- a/proto/zitadel/object/v3alpha/object.proto +++ b/proto/zitadel/object/v3alpha/object.proto @@ -27,3 +27,11 @@ message Instance { string domain = 2; } } + +message Organization { + oneof property { + option (validate.required) = true; + string org_id = 1; + string org_domain = 2; + } +} \ No newline at end of file diff --git a/proto/zitadel/resources/action/v3alpha/action_service.proto b/proto/zitadel/resources/action/v3alpha/action_service.proto index 228899310..1254dddf8 100644 --- a/proto/zitadel/resources/action/v3alpha/action_service.proto +++ b/proto/zitadel/resources/action/v3alpha/action_service.proto @@ -17,7 +17,6 @@ import "zitadel/resources/action/v3alpha/query.proto"; import "zitadel/resources/object/v3alpha/object.proto"; import "zitadel/object/v3alpha/object.proto"; - option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { diff --git a/proto/zitadel/user/v3alpha/authenticator.proto b/proto/zitadel/resources/user/v3alpha/authenticator.proto similarity index 84% rename from proto/zitadel/user/v3alpha/authenticator.proto rename to proto/zitadel/resources/user/v3alpha/authenticator.proto index 7527bcdb6..827f67c33 100644 --- a/proto/zitadel/user/v3alpha/authenticator.proto +++ b/proto/zitadel/resources/user/v3alpha/authenticator.proto @@ -1,15 +1,15 @@ syntax = "proto3"; -package zitadel.user.v3alpha; +package zitadel.resources.user.v3alpha; import "google/api/field_behavior.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user"; message Authenticators { // All of the user's usernames, which will be used for identification during authentication. @@ -109,6 +109,50 @@ message WebAuthN { bool user_verified = 4; } +message StartWebAuthNRegistration { + // Domain on which the user currently is or will be authenticated. + string domain = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"my-domain.zitadel.cloud\""; + } + ]; + // Optionally specify the authenticator type of the passkey device (platform or cross-platform). + // If none is provided, both values are allowed. + WebAuthNAuthenticatorType authenticator_type = 2; + // Optionally provide a one time code generated by ZITADEL. + // This is required to start the passkey registration without user authentication. + optional AuthenticatorRegistrationCode code = 3; +} + +message VerifyWebAuthNRegistration { + // PublicKeyCredential Interface. + // Generated helper methods populate the field from JSON created by a WebAuthN client. + // See also: https://www.w3.org/TR/webauthn/#publickeycredential + google.protobuf.Struct public_key_credential = 1 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"type\":\"public-key\",\"id\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"rawId\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"response\":{\"attestationObject\":\"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgRKS3VpeE9tfExXRzkoUKnG4rQWPvtSSt4YtDGgTx32oCIQDPey-2YJ4uIg-QCM4jj6aE2U3tgMFM_RP7Efx6xRu3JGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAADju76085Yhmlt1CEOHkwLQAIKWsFWqxeMT8SxZnwp0ZMF1nk6yhs2m3AIvdixCNVgtNpQECAyYgASFYIMGUDSP2FAQn2MIfPMy7cyB_Y30VqixVgGULTBtFjfRiIlggjUGfQo3_-CrMmH3S-ZQkFKWKnNBQEAMkFtG-9A4zqW0\",\"clientDataJSON\":\"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQlhXdHh0WGxJeFZZa0pHT1dVaUVmM25zby02aXZKdWw2YmNmWHdMVlFIayIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjgwODAifQ\"}}"; + min_length: 55; + max_length: 1048576; //1 MB + } + ]; + // Provide a name for the WebAuthN device. This will help identify it in the future. + string web_auth_n_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"fido key\"" + } + ]; +} + message OTPSMS { // unique identifier of the one-time-password (OTP) SMS authenticator. string otp_sms_id = 1 [ @@ -167,7 +211,7 @@ message AuthenticationKey { example: "\"69629023906488334\""; } ]; - zitadel.object.v2.Details details = 2; + zitadel.resources.object.v3alpha.Details details = 2; // the file type of the key AuthNKeyType type = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -246,6 +290,30 @@ message SetPassword { } // Provide if the user needs to change the password on the next use. bool change_required = 3; + // If neither, the current password nor a verification code generated by the PasswordReset is provided, + // the user must be granted permission to set a password. + oneof verification { + // Provide the current password to verify you're allowed to change the password. + string current_password = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Secr3tP4ssw0rd!\""; + } + ]; + // Or provider the verification code generated during password reset request. + string verification_code = 5 [ + (validate.rules).string = {min_len: 1, max_len: 20}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 20; + example: "\"SKJd342k\""; + } + ]; + } } message SendPasswordResetEmail { diff --git a/proto/zitadel/user/v3alpha/communication.proto b/proto/zitadel/resources/user/v3alpha/communication.proto similarity index 96% rename from proto/zitadel/user/v3alpha/communication.proto rename to proto/zitadel/resources/user/v3alpha/communication.proto index af799f411..b27379cf1 100644 --- a/proto/zitadel/user/v3alpha/communication.proto +++ b/proto/zitadel/resources/user/v3alpha/communication.proto @@ -1,8 +1,8 @@ syntax = "proto3"; -package zitadel.user.v3alpha; +package zitadel.resources.user.v3alpha; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user"; import "google/api/field_behavior.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; diff --git a/proto/zitadel/user/v3alpha/query.proto b/proto/zitadel/resources/user/v3alpha/query.proto similarity index 69% rename from proto/zitadel/user/v3alpha/query.proto rename to proto/zitadel/resources/user/v3alpha/query.proto index 4e2bf062b..4fffe517e 100644 --- a/proto/zitadel/user/v3alpha/query.proto +++ b/proto/zitadel/resources/user/v3alpha/query.proto @@ -1,72 +1,71 @@ syntax = "proto3"; -package zitadel.user.v3alpha; +package zitadel.resources.user.v3alpha; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user"; import "google/api/field_behavior.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/user/v3alpha/user.proto"; -import "zitadel/object/v2/object.proto"; +import "zitadel/resources/user/v3alpha/user.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; -message SearchQuery { - oneof query { +message SearchFilter { + oneof Filter { option (validate.required) = true; - - // Union the results of each sub query ('OR'). - OrQuery or_query = 1; + // Union the results of each sub filter ('OR'). + OrFilter or_filter = 1; // Limit the result to match all sub queries ('AND'). // Note that if you specify multiple queries, they will be implicitly used as andQueries. - // Use the andQuery in combination with orQuery and notQuery. - AndQuery and_query = 2; + // Use the andFilter in combination with orFilter and notFilter. + AndFilter and_filter = 2; // Exclude / Negate the result of the sub query ('NOT'). - NotQuery not_query = 3; + NotFilter not_filter = 3; // Limit the result to a specific user ID. - UserIDQuery user_id_query = 4; + UserIDFilter user_id_filter = 4; // Limit the result to a specific organization. - OrganizationIDQuery organization_id_query = 5; + OrganizationIDFilter organization_id_filter = 5; // Limit the result to a specific username. - UsernameQuery username_query = 6; + UsernameFilter username_filter = 6; // Limit the result to a specific contact email. - EmailQuery email_query = 7; + EmailFilter email_filter = 7; // Limit the result to a specific contact phone. - PhoneQuery phone_query = 8; + PhoneFilter phone_filter = 8; // Limit the result to a specific state of the user. - StateQuery state_query = 9; + StateFilter state_filter = 9; // Limit the result to a specific schema ID. - SchemaIDQuery schema_ID_query = 10; + SchemaIDFilter schema_id_filter = 10; // Limit the result to a specific schema type. - SchemaTypeQuery schema_type_query = 11; + SchemaTypeFilter schema_type_filter = 11; } } -message OrQuery { - repeated SearchQuery queries = 1 [ +message OrFilter { + repeated SearchFilter queries = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[{\"userIdQuery\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}},{\"userIdQuery\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}}]" + example: "[{\"userIdFilter\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}},{\"userIdFilter\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}}]" } ]; } -message AndQuery { - repeated SearchQuery queries = 1 [ +message AndFilter { + repeated SearchFilter queries = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[{\"organizationIdQuery\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}},{\"usernameQuery\": {\"username\": \"gigi\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}}]" + example: "[{\"organizationIdFilter\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}},{\"usernameFilter\": {\"username\": \"gigi\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}}]" } ]; } -message NotQuery { - SearchQuery query = 1 [ +message NotFilter { + SearchFilter query = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"schemaIDQuery\": {\"id\": \"163840776835432705\"}}" + example: "{\"schemaIDFilter\": {\"id\": \"163840776835432705\"}}" } ]; } -message UserIDQuery { +message UserIDFilter { // Defines the ID of the user to query for. string id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -78,12 +77,12 @@ message UserIDQuery { } ]; // Defines which text comparison method used for the id query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } -message OrganizationIDQuery { +message OrganizationIDFilter { // Defines the ID of the organization to query for. string id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -95,12 +94,12 @@ message OrganizationIDQuery { } ]; // Defines which text comparison method used for the id query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } -message UsernameQuery { +message UsernameFilter { // Defines the username to query for. string username = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -112,14 +111,14 @@ message UsernameQuery { } ]; // Defines which text comparison method used for the username query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; // Defines that the username must only be unique in the organisation. bool is_organization_specific = 3; } -message EmailQuery { +message EmailFilter { // Defines the email of the user to query for. string address = 1 [ (validate.rules).string = {max_len: 200}, @@ -131,12 +130,12 @@ message EmailQuery { } ]; // Defines which text comparison method used for the email query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } -message PhoneQuery { +message PhoneFilter { // Defines the phone of the user to query for. string number = 1 [ (validate.rules).string = {min_len: 1, max_len: 20}, @@ -148,13 +147,13 @@ message PhoneQuery { } ]; // Defines which text comparison method used for the phone query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } -message StateQuery { +message StateFilter { // Defines the state to query for. State state = 1 [ (validate.rules).enum.defined_only = true, @@ -164,7 +163,7 @@ message StateQuery { ]; } -message SchemaIDQuery { +message SchemaIDFilter { // Defines the ID of the schema to query for. string id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -177,7 +176,7 @@ message SchemaIDQuery { ]; } -message SchemaTypeQuery { +message SchemaTypeFilter { // Defines which type to query for. string type = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -189,7 +188,7 @@ message SchemaTypeQuery { } ]; // Defines which text comparison method used for the type query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } diff --git a/proto/zitadel/resources/user/v3alpha/user.proto b/proto/zitadel/resources/user/v3alpha/user.proto new file mode 100644 index 000000000..1be5f3c3f --- /dev/null +++ b/proto/zitadel/resources/user/v3alpha/user.proto @@ -0,0 +1,112 @@ +syntax = "proto3"; + +package zitadel.resources.user.v3alpha; + +import "google/api/field_behavior.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; +import "zitadel/resources/user/v3alpha/authenticator.proto"; +import "zitadel/resources/user/v3alpha/communication.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user"; + +message CreateUser { + // Define the schema the user's data schema by providing it's ID. + string schema_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + google.protobuf.Struct data = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}" + } + ]; + // Set the contact information (email, phone) for the user. + SetContact contact = 3; + // Set the initial authenticators of the user. + SetAuthenticators authenticators = 4; + // Optionally set a unique identifier of the user. If unset, ZITADEL will take care of it. + optional string user_id = 5 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message PatchUser { + optional string schema_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + optional google.protobuf.Struct data = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}" + } + ]; + // Set the contact information (email, phone) for the user. + optional SetContact contact = 3; + // TODO: No SetAuthenticators? +} + +message GetUser{ + // Details provide some base information (such as the last change date) of the user. + zitadel.resources.object.v3alpha.Details details = 1; + // The schema the user and it's data is based on. + GetSchema schema = 2; + google.protobuf.Struct data = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}" + } + ]; + // Contact information for the user. ZITADEL will use this in case of internal notifications. + Contact contact = 4; + // The user's authenticators. They are used to identify and authenticate the user + // during the authentication process. + Authenticators authenticators = 5; + // State of the user. + State state = 6; +} + +enum State { + USER_STATE_UNSPECIFIED = 0; + USER_STATE_ACTIVE = 1; + USER_STATE_INACTIVE = 2; + USER_STATE_DELETED = 3; + USER_STATE_LOCKED = 4; +} + +message GetSchema { + // The unique identifier of the user schema. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629026806489455\"" + } + ]; + // The human readable name of the user schema. + string type = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"employees\""; + } + ]; + // The revision the user's data is based on of the revision. + uint32 revision = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "7"; + } + ]; +} diff --git a/proto/zitadel/user/v3alpha/user_service.proto b/proto/zitadel/resources/user/v3alpha/user_service.proto similarity index 74% rename from proto/zitadel/user/v3alpha/user_service.proto rename to proto/zitadel/resources/user/v3alpha/user_service.proto index b193cc52c..e99f28af0 100644 --- a/proto/zitadel/user/v3alpha/user_service.proto +++ b/proto/zitadel/resources/user/v3alpha/user_service.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package zitadel.user.v3alpha; +package zitadel.resources.user.v3alpha; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; @@ -8,20 +8,20 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; +import "zitadel/object/v3alpha/object.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; -import "zitadel/user/v3alpha/authenticator.proto"; -import "zitadel/user/v3alpha/communication.proto"; -import "zitadel/user/v3alpha/query.proto"; -import "zitadel/user/v3alpha/user.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha"; +import "zitadel/resources/user/v3alpha/authenticator.proto"; +import "zitadel/resources/user/v3alpha/communication.proto"; +import "zitadel/resources/user/v3alpha/query.proto"; +import "zitadel/resources/user/v3alpha/user.proto"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "User Service"; - version: "3.0-preview"; + version: "3.0-alpha"; description: "This API is intended to manage users with your own data schema in a ZITADEL instance. This project is in preview state. It can AND will continue breaking until the service provides the same functionality as the v1 and v2 user services."; contact:{ name: "ZITADEL" @@ -46,7 +46,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { produces: "application/grpc-web+proto"; host: "$CUSTOM-DOMAIN"; - base_path: "/"; + base_path: "/resources/v3alpha/users"; external_docs: { description: "Detailed information about ZITADEL", @@ -106,16 +106,16 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } }; -service UserService { +service ZITADELUsers { - // List users + // Search users // - // List all matching users. By default, we will return all users of your instance. + // Search all matching users. By default, we will return all users of your instance. // Make sure to include a limit and sorting for pagination. - rpc ListUsers (ListUsersRequest) returns (ListUsersResponse) { + rpc SearchUsers (SearchUsersRequest) returns (SearchUsersResponse) { option (google.api.http) = { - post: "/v3alpha/users/search" - body: "*" + post: "/_search" + body: "filters" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -148,9 +148,9 @@ service UserService { // User by ID // // Returns the user identified by the requested ID. - rpc GetUserByID (GetUserByIDRequest) returns (GetUserByIDResponse) { + rpc GetUser (GetUserRequest) returns (GetUserResponse) { option (google.api.http) = { - get: "/v3alpha/users/{user_id}" + get: "/{user_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -174,13 +174,13 @@ service UserService { // Create a new user with an optional data schema. rpc CreateUser (CreateUserRequest) returns (CreateUserResponse) { option (google.api.http) = { - post: "/v3alpha/users" - body: "*" + post: "/" + body: "user" }; option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "user.write" + permission: "authenticated" org_field: "organization" } http_response: { @@ -203,13 +203,13 @@ service UserService { }; } - // Update a user + // Patch a user // - // Update an existing user with data based on a user schema. - rpc UpdateUser (UpdateUserRequest) returns (UpdateUserResponse) { + // Patch an existing user with data based on a user schema. + rpc PatchUser (PatchUserRequest) returns (PatchUserResponse) { option (google.api.http) = { - put: "/v3alpha/users/{user_id}" - body: "*" + patch: "/{user_id}" + body: "user" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -222,7 +222,7 @@ service UserService { responses: { key: "200"; value: { - description: "User successfully updated"; + description: "User successfully updated or left unchanged"; }; }; }; @@ -238,7 +238,7 @@ service UserService { // The endpoint returns an error if the user is already in the state 'deactivated'. rpc DeactivateUser (DeactivateUserRequest) returns (DeactivateUserResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/deactivate" + post: "/{user_id}/_deactivate" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -265,7 +265,7 @@ service UserService { // The endpoint returns an error if the user is not in the state 'deactivated'. rpc ReactivateUser (ReactivateUserRequest) returns (ReactivateUserResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/reactivate" + post: "/{user_id}/_reactivate" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -294,7 +294,7 @@ service UserService { // The endpoint returns an error if the user is already in the state 'locked'. rpc LockUser (LockUserRequest) returns (LockUserResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/lock" + post: "/{user_id}/_lock" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -321,7 +321,7 @@ service UserService { // The endpoint returns an error if the user is not in the state 'locked'. rpc UnlockUser (UnlockUserRequest) returns (UnlockUserResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/unlock" + post: "/{user_id}/_unlock" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -346,7 +346,7 @@ service UserService { // The user will be able to log in anymore. rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse) { option (google.api.http) = { - delete: "/v3alpha/users/{user_id}" + delete: "/{user_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -372,8 +372,8 @@ service UserService { // which can be either returned or will be sent to the user by email. rpc SetContactEmail (SetContactEmailRequest) returns (SetContactEmailResponse) { option (google.api.http) = { - put: "/v3alpha/users/{user_id}/email" - body: "*" + put: "/{user_id}/email" + body: "email" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -397,8 +397,8 @@ service UserService { // Verify the contact email with the provided code. rpc VerifyContactEmail (VerifyContactEmailRequest) returns (VerifyContactEmailResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/email/verify" - body: "*" + post: "/{user_id}/email/_verify" + body: "verification_code" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -422,7 +422,7 @@ service UserService { // Resend the email with the verification code for the contact email address. rpc ResendContactEmailCode (ResendContactEmailCodeRequest) returns (ResendContactEmailCodeResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/email/resend" + post: "/{user_id}/email/_resend" body: "*" }; @@ -449,8 +449,8 @@ service UserService { // which can be either returned or will be sent to the user by SMS. rpc SetContactPhone (SetContactPhoneRequest) returns (SetContactPhoneResponse) { option (google.api.http) = { - put: "/v3alpha/users/{user_id}/phone" - body: "*" + put: "/{user_id}/phone" + body: "phone" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -474,8 +474,8 @@ service UserService { // Verify the contact phone with the provided code. rpc VerifyContactPhone (VerifyContactPhoneRequest) returns (VerifyContactPhoneResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/phone/verify" - body: "*" + post: "/{user_id}/phone/_verify" + body: "verification_code" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -499,7 +499,7 @@ service UserService { // Resend the phone with the verification code for the contact phone number. rpc ResendContactPhoneCode (ResendContactPhoneCodeRequest) returns (ResendContactPhoneCodeResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/phone/resend" + post: "/{user_id}/phone/_resend" body: "*" }; @@ -524,8 +524,8 @@ service UserService { // 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: "/v3alpha/users/{user_id}/username" - body: "*" + post: "/{user_id}/username" + body: "username" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -549,7 +549,7 @@ service UserService { // 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: "/v3alpha/users/{user_id}/username/{username_id}" + delete: "/{user_id}/username/{username_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -573,8 +573,8 @@ service UserService { // 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: "/v2/users/{user_id}/password" - body: "*" + post: "/{user_id}/password" + body: "new_password" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -598,7 +598,7 @@ service UserService { // Request a code to be able to set a new password. rpc RequestPasswordReset (RequestPasswordResetRequest) returns (RequestPasswordResetResponse) { option (google.api.http) = { - post: "/v2/users/{user_id}/password/reset" + post: "/{user_id}/password/_reset" body: "*" }; @@ -625,8 +625,8 @@ service UserService { // which are used to verify the device. rpc StartWebAuthNRegistration (StartWebAuthNRegistrationRequest) returns (StartWebAuthNRegistrationResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/webauthn" - body: "*" + post: "/{user_id}/webauthn" + body: "registration" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -650,8 +650,8 @@ service UserService { // Verify the WebAuthN registration started by StartWebAuthNRegistration with the public key credential. rpc VerifyWebAuthNRegistration (VerifyWebAuthNRegistrationRequest) returns (VerifyWebAuthNRegistrationResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/webauthn/{web_auth_n_id}" - body: "*" + post: "/{user_id}/webauthn/{web_auth_n_id}/_verify" + body: "verify" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -675,7 +675,7 @@ service UserService { // The code will allow the user to start a new WebAuthN registration. rpc CreateWebAuthNRegistrationLink (CreateWebAuthNRegistrationLinkRequest) returns (CreateWebAuthNRegistrationLinkResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/webauthn/registration_link" + post: "/{user_id}/webauthn/registration_link" body: "*" }; @@ -699,7 +699,7 @@ service UserService { // 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: "/v3alpha/users/{user_id}/webauthn/{web_auth_n_id}" + delete: "/{user_id}/webauthn/{web_auth_n_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -723,7 +723,7 @@ service UserService { // 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: "/v3alpha/users/{user_id}/totp" + post: "/{user_id}/totp" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -746,8 +746,8 @@ service UserService { // Verify the time-based one-time-password (TOTP) registration with the generated code. rpc VerifyTOTPRegistration (VerifyTOTPRegistrationRequest) returns (VerifyTOTPRegistrationResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/totp/{totp_id}/verify" - body: "*" + post: "/{user_id}/totp/{totp_id}/_verify" + body: "code" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -770,7 +770,7 @@ service UserService { // 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: "/v3alpha/users/{user_id}/totp/{totp_id}" + delete: "/{user_id}/totp/{totp_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -795,8 +795,8 @@ service UserService { // which can be either returned or will be sent to the user by SMS. rpc AddOTPSMSAuthenticator (AddOTPSMSAuthenticatorRequest) returns (AddOTPSMSAuthenticatorResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/otp_sms" - body: "*" + post: "/{user_id}/otp_sms" + body: "phone" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -819,8 +819,8 @@ service UserService { // Verify the OTP SMS registration with the provided code. rpc VerifyOTPSMSRegistration (VerifyOTPSMSRegistrationRequest) returns (VerifyOTPSMSRegistrationResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/otp_sms/{otp_sms_id}/verify" - body: "*" + post: "/{user_id}/otp_sms/{otp_sms_id}/_verify" + body: "code" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -844,7 +844,7 @@ service UserService { // 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: "/v3alpha/users/{user_id}/otp_sms/{otp_sms_id}" + delete: "/{user_id}/otp_sms/{otp_sms_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -869,8 +869,8 @@ service UserService { // which can be either returned or will be sent to the user by email. rpc AddOTPEmailAuthenticator (AddOTPEmailAuthenticatorRequest) returns (AddOTPEmailAuthenticatorResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/otp_email" - body: "*" + post: "/{user_id}/otp_email" + body: "email" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -893,8 +893,8 @@ service UserService { // Verify the OTP Email registration with the provided code. rpc VerifyOTPEmailRegistration (VerifyOTPEmailRegistrationRequest) returns (VerifyOTPEmailRegistrationResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/otp_email/{otp_email_id}/verify" - body: "*" + post: "/{user_id}/otp_email/{otp_email_id}/_verify" + body: "code" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -918,7 +918,7 @@ service UserService { // 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: "/v3alpha/users/{user_id}/otp_email/{otp_email_id}" + delete: "/{user_id}/otp_email/{otp_email_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -961,13 +961,12 @@ service UserService { }; } - // Retrieve the information of the IDP authentication intent + // Get the information of the IDP authentication intent // - // Retrieve the information returned by the identity provider (IDP) for registration or updating an existing user with new information. - rpc RetrieveIdentityProviderIntent (RetrieveIdentityProviderIntentRequest) returns (RetrieveIdentityProviderIntentResponse) { + // Get the information returned by the identity provider (IDP) for registration or updating an existing user with new information. + rpc GetIdentityProviderIntent (GetIdentityProviderIntentRequest) returns (GetIdentityProviderIntentResponse) { option (google.api.http) = { - post: "/v3alpha/idp_intents/{idp_intent_id}" - body: "*" + get: "/v3alpha/idp_intents/{idp_intent_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -992,8 +991,8 @@ service UserService { // This will allow the user to authenticate with the provided IDP. rpc AddIDPAuthenticator (AddIDPAuthenticatorRequest) returns (AddIDPAuthenticatorResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/idps" - body: "*" + post: "/{user_id}/idps" + body: "authenticator" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -1017,7 +1016,7 @@ service UserService { // 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: "/v3alpha/users/{user_id}/idps/{idp_id}" + delete: "/{user_id}/idps/{idp_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -1037,31 +1036,40 @@ service UserService { } -message ListUsersRequest { - // list limitations and ordering. - zitadel.object.v2.ListQuery query = 1; +message SearchUsersRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + + // Search limitations and ordering. + zitadel.resources.object.v3alpha.SearchQuery query = 2; // the field the result is sorted. - zitadel.user.v3alpha.FieldName sorting_column = 2 [ + FieldName sorting_column = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"FIELD_NAME_SCHEMA_TYPE\"" } ]; // Define the criteria to query for. - repeated zitadel.user.v3alpha.SearchQuery queries = 3; + repeated SearchFilter filters = 4; } -message ListUsersResponse { +message SearchUsersResponse { // Details provides information about the returned result including total amount found. - zitadel.object.v2.ListDetails details = 1; - // States by which field the results are sorted. - zitadel.user.v3alpha.FieldName sorting_column = 2; + zitadel.resources.object.v3alpha.ListDetails details = 1; // The result contains the user schemas, which matched the queries. - repeated zitadel.user.v3alpha.User result = 3; + repeated GetUser result = 2; } -message GetUserByIDRequest { +message GetUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; // unique identifier of the user. - string user_id = 1 [ + string user_id = 2 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1072,100 +1080,67 @@ message GetUserByIDRequest { ]; } -message GetUserByIDResponse { - zitadel.user.v3alpha.User user = 1; +message GetUserResponse { + GetUser user = 1; } message CreateUserRequest { - // Optionally set a unique identifier of the user. If unset, ZITADEL will take care of it. - optional string user_id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + optional zitadel.object.v3alpha.Instance instance = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; + default: "\"domain from HOST or :authority header\"" } ]; // Set the organization the user belongs to. - zitadel.object.v2.Organization organization = 2 [ + zitadel.object.v3alpha.Organization organization = 2 [ (validate.rules).message = {required: true}, (google.api.field_behavior) = REQUIRED ]; - // Set the initial authenticators of the user. - SetAuthenticators authenticators = 3; - // Set the contact information (email, phone) for the user. - SetContact contact = 4; - // Define the schema the user's data schema by providing it's ID. - string schema_id = 5 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; - // Provide data about the user. It will be validated based on the specified schema. - google.protobuf.Struct data = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}" - } - ]; + CreateUser user = 3; } message CreateUserResponse { - string user_id = 1; - zitadel.object.v2.Details details = 2; + zitadel.resources.object.v3alpha.Details details = 1; // The email code will be set if a contact email was set with a return_code verification option. - optional string email_code = 3 [ + optional string email_code = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"SKJd342k\""; } ]; // The phone code will be set if a contact phone was set with a return_code verification option. - optional string phone_code = 4 [ + optional string phone_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"IFi39dk2\""; } ]; } -message UpdateUserRequest { +message PatchUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"69629012906488334\""; } ]; - // Add or update the contact information (email, phone) for the user if needed. - optional SetContact contact = 4; - // Change the schema the user's data schema by providing it's ID if needed. - optional string schema_id = 5 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; - // Update the user data if needed. It will be validated based on the specified schema. - optional google.protobuf.Struct data = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}" - } - ]; + PatchUser user = 4; } -message UpdateUserResponse { - zitadel.object.v2.Details details = 1; +message PatchUserResponse { + zitadel.resources.object.v3alpha.Details details = 1; // The email code will be set if a contact email was set with a return_code verification option. - optional string email_code = 3 [ + optional string email_code = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"SKJd342k\""; } ]; // The phone code will be set if a contact phone was set with a return_code verification option. - optional string phone_code = 4 [ + optional string phone_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"IFi39dk2\""; } @@ -1173,8 +1148,15 @@ message UpdateUserResponse { } message DeactivateUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1186,13 +1168,20 @@ message DeactivateUserRequest { } message DeactivateUserResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message ReactivateUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1204,12 +1193,19 @@ message ReactivateUserRequest { } message ReactivateUserResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message LockUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1221,12 +1217,19 @@ message LockUserRequest { } message LockUserResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message UnlockUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1238,12 +1241,19 @@ message UnlockUserRequest { } message UnlockUserResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message DeleteUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1255,12 +1265,19 @@ message DeleteUserRequest { } message DeleteUserResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message SetContactEmailRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1270,11 +1287,11 @@ message SetContactEmailRequest { } ]; // Set the user's contact email and it's verification state. - SetEmail email = 2; + SetEmail email = 4; } message SetContactEmailResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // The verification code will be set if a contact email was set with a return_code verification option. optional string verification_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1284,8 +1301,15 @@ message SetContactEmailResponse { } message VerifyContactEmailRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1295,7 +1319,7 @@ message VerifyContactEmailRequest { } ]; // Set the verification code generated during the set contact email request. - string verification_code = 2 [ + string verification_code = 4 [ (validate.rules).string = {min_len: 1, max_len: 20}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1307,12 +1331,19 @@ message VerifyContactEmailRequest { } message VerifyContactEmailResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message ResendContactEmailCodeRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1324,21 +1355,28 @@ message ResendContactEmailCodeRequest { // if no verification is specified, an email is sent oneof verification { // Let ZITADEL send the link to the user via email. - SendEmailVerificationCode send_code = 2; + SendEmailVerificationCode send_code = 4; // Get the code back to provide it to the user in your preferred mechanism. - ReturnEmailVerificationCode return_code = 3; + ReturnEmailVerificationCode return_code = 5; } } message ResendContactEmailCodeResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // in case the verification was set to return_code, the code will be returned. optional string verification_code = 2; } message SetContactPhoneRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1348,11 +1386,11 @@ message SetContactPhoneRequest { } ]; // Set the user's contact phone and it's verification state. - SetPhone phone = 2; + SetPhone phone = 4; } message SetContactPhoneResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // The phone verification code will be set if a contact phone was set with a return_code verification option. optional string email_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1362,8 +1400,15 @@ message SetContactPhoneResponse { } message VerifyContactPhoneRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1373,7 +1418,7 @@ message VerifyContactPhoneRequest { } ]; // Set the verification code generated during the set contact phone request. - string verification_code = 2 [ + string verification_code = 4 [ (validate.rules).string = {min_len: 1, max_len: 20}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1385,12 +1430,19 @@ message VerifyContactPhoneRequest { } message VerifyContactPhoneResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message ResendContactPhoneCodeRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1402,21 +1454,28 @@ message ResendContactPhoneCodeRequest { // if no verification is specified, a SMS is sent oneof verification { // Let ZITADEL send the link to the user via SMS. - SendPhoneVerificationCode send_code = 2; + SendPhoneVerificationCode send_code = 4; // Get the code back to provide it to the user in your preferred mechanism. - ReturnPhoneVerificationCode return_code = 3; + ReturnPhoneVerificationCode return_code = 5; } } message ResendContactPhoneCodeResponse { - zitadel.object.v2.Details details = 1; - // in case the verification was set to return_code, the code will be returned. - optional string verification_code = 2; + zitadel.resources.object.v3alpha.Details details = 1; + // in case the verification was set to return_code, the code will be returned. + optional string verification_code = 2; } message AddUsernameRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1426,11 +1485,11 @@ message AddUsernameRequest { } ]; // Set the user's new username. - SetUsername username = 2; + SetUsername username = 4; } message AddUsernameResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // unique identifier of the username. string username_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1440,8 +1499,15 @@ message AddUsernameResponse { } message RemoveUsernameRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1451,7 +1517,7 @@ message RemoveUsernameRequest { } ]; // unique identifier of the username. - string username_id = 2 [ + string username_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1463,12 +1529,19 @@ message RemoveUsernameRequest { } message RemoveUsernameResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message SetPasswordRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1478,40 +1551,23 @@ message SetPasswordRequest { } ]; // Provide the new password (in plain text or as hash). - SetPassword new_password = 2; - // If neither, the current password nor a verification code generated by the PasswordReset is provided, - // the user must be granted permission to set a password. - oneof verification { - // Provide the current password to verify you're allowed to change the password. - string current_password = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1; - max_length: 200; - example: "\"Secr3tP4ssw0rd!\""; - } - ]; - // Or provider the verification code generated during password reset request. - string verification_code = 4 [ - (validate.rules).string = {min_len: 1, max_len: 20}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1; - max_length: 20; - example: "\"SKJd342k\""; - } - ]; - } + SetPassword new_password = 4; } message SetPasswordResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message RequestPasswordResetRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1523,16 +1579,16 @@ message RequestPasswordResetRequest { // If no medium is specified, an email is sent with the default url. oneof medium { // Let ZITADEL send the link to the user via email. - SendPasswordResetEmail send_email = 2; + SendPasswordResetEmail send_email = 4; // Let ZITADEL send the link to the user via SMS. - SendPasswordResetSMS send_sms = 3; + SendPasswordResetSMS send_sms = 5; // Get the code back to provide it to the user in your preferred mechanism. - ReturnPasswordResetCode return_code = 4; + ReturnPasswordResetCode return_code = 6; } } message RequestPasswordResetResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // In case the medium was set to return_code, the code will be returned. optional string verification_code = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1542,8 +1598,15 @@ message RequestPasswordResetResponse { } message StartWebAuthNRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1552,26 +1615,11 @@ message StartWebAuthNRegistrationRequest { example: "\"69629026806489455\""; } ]; - // Domain on which the user currently is or will be authenticated. - string domain = 4 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"my-domain.zitadel.cloud\""; - } - ]; - // Optionally specify the authenticator type of the passkey device (platform or cross-platform). - // If none is provided, both values are allowed. - WebAuthNAuthenticatorType authenticator_type = 3; - // Optionally provide a one time code generated by ZITADEL. - // This is required to start the passkey registration without user authentication. - optional AuthenticatorRegistrationCode code = 2; + StartWebAuthNRegistration registration = 4; } message StartWebAuthNRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // unique identifier of the WebAuthN registration. string web_auth_n_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1589,8 +1637,15 @@ message StartWebAuthNRegistrationResponse { } message VerifyWebAuthNRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1600,7 +1655,7 @@ message VerifyWebAuthNRegistrationRequest { } ]; // unique identifier of the WebAuthN registration, which was returned in the start webauthn registration. - string web_auth_n_id = 2 [ + string web_auth_n_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1609,37 +1664,23 @@ message VerifyWebAuthNRegistrationRequest { example: "\"163840776835432705\""; } ]; - // PublicKeyCredential Interface. - // Generated helper methods populate the field from JSON created by a WebAuthN client. - // See also: https://www.w3.org/TR/webauthn/#publickeycredential - google.protobuf.Struct public_key_credential = 3 [ - (validate.rules).message.required = true, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"type\":\"public-key\",\"id\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"rawId\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"response\":{\"attestationObject\":\"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgRKS3VpeE9tfExXRzkoUKnG4rQWPvtSSt4YtDGgTx32oCIQDPey-2YJ4uIg-QCM4jj6aE2U3tgMFM_RP7Efx6xRu3JGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAADju76085Yhmlt1CEOHkwLQAIKWsFWqxeMT8SxZnwp0ZMF1nk6yhs2m3AIvdixCNVgtNpQECAyYgASFYIMGUDSP2FAQn2MIfPMy7cyB_Y30VqixVgGULTBtFjfRiIlggjUGfQo3_-CrMmH3S-ZQkFKWKnNBQEAMkFtG-9A4zqW0\",\"clientDataJSON\":\"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQlhXdHh0WGxJeFZZa0pHT1dVaUVmM25zby02aXZKdWw2YmNmWHdMVlFIayIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjgwODAifQ\"}}"; - min_length: 55; - max_length: 1048576; //1 MB - } - ]; - // Provide a name for the WebAuthN device. This will help identify it in the future. - string web_auth_n_name = 4 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1; - max_length: 200; - example: "\"fido key\"" - } - ]; + VerifyWebAuthNRegistration verify = 5; } message VerifyWebAuthNRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message CreateWebAuthNRegistrationLinkRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1651,21 +1692,28 @@ message CreateWebAuthNRegistrationLinkRequest { // if no medium is specified, an email is sent with the default url. oneof medium { // Let ZITADEL send the link to the user via email. - SendWebAuthNRegistrationLink send_link = 2; + SendWebAuthNRegistrationLink send_link = 4; // Get the code back to provide it to the user in your preferred mechanism. - ReturnWebAuthNRegistrationCode return_code = 3; + ReturnWebAuthNRegistrationCode return_code = 5; } } message CreateWebAuthNRegistrationLinkResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // In case the medium was set to return_code, the code will be returned. optional AuthenticatorRegistrationCode code = 2; } message RemoveWebAuthNAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1675,7 +1723,7 @@ message RemoveWebAuthNAuthenticatorRequest { } ]; // unique identifier of the WebAuthN authenticator. - string web_auth_n_id = 2 [ + string web_auth_n_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1687,12 +1735,19 @@ message RemoveWebAuthNAuthenticatorRequest { } message RemoveWebAuthNAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message StartTOTPRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1704,7 +1759,7 @@ message StartTOTPRegistrationRequest { } message StartTOTPRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // unique identifier of the TOTP registration. string totp_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1726,8 +1781,15 @@ message StartTOTPRegistrationResponse { } message VerifyTOTPRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1737,7 +1799,7 @@ message VerifyTOTPRegistrationRequest { } ]; // unique identifier of the TOTP registration, which was returned in the start TOTP registration. - string totp_id = 2 [ + string totp_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1747,7 +1809,7 @@ message VerifyTOTPRegistrationRequest { } ]; // Code generated by TOTP app or device. - string code = 3 [ + string code = 5 [ (validate.rules).string = {min_len: 6, max_len: 9}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1759,12 +1821,19 @@ message VerifyTOTPRegistrationRequest { } message VerifyTOTPRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message RemoveTOTPAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1774,7 +1843,7 @@ message RemoveTOTPAuthenticatorRequest { } ]; // unique identifier of the TOTP authenticator. - string totp_id = 2 [ + string totp_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1786,12 +1855,19 @@ message RemoveTOTPAuthenticatorRequest { } message RemoveTOTPAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message AddOTPSMSAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1801,11 +1877,11 @@ message AddOTPSMSAuthenticatorRequest { } ]; // Set the user's phone for the OTP SMS authenticator and it's verification state. - SetPhone phone = 2; + SetPhone phone = 4; } message AddOTPSMSAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // unique identifier of the OTP SMS registration. string otp_sms_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1822,8 +1898,15 @@ message AddOTPSMSAuthenticatorResponse { } message VerifyOTPSMSRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_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 +1916,7 @@ message VerifyOTPSMSRegistrationRequest { } ]; // unique identifier of the OTP SMS registration, which was returned in the add OTP SMS. - string otp_sms_id = 2 [ + string otp_sms_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1843,7 +1926,7 @@ message VerifyOTPSMSRegistrationRequest { } ]; // Set the verification code generated during the add OTP SMS request. - string code = 3 [ + string code = 5 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1855,12 +1938,19 @@ message VerifyOTPSMSRegistrationRequest { } message VerifyOTPSMSRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message RemoveOTPSMSAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1870,7 +1960,7 @@ message RemoveOTPSMSAuthenticatorRequest { } ]; // unique identifier of the OTP SMS authenticator. - string otp_sms_id = 2 [ + string otp_sms_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1882,12 +1972,19 @@ message RemoveOTPSMSAuthenticatorRequest { } message RemoveOTPSMSAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message AddOTPEmailAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1897,11 +1994,11 @@ message AddOTPEmailAuthenticatorRequest { } ]; // Set the user's email for the OTP Email authenticator and it's verification state. - SetEmail email = 2; + SetEmail email = 4; } message AddOTPEmailAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // unique identifier of the OTP Email registration. string otp_email_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1917,8 +2014,15 @@ message AddOTPEmailAuthenticatorResponse { } message VerifyOTPEmailRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1928,7 +2032,7 @@ message VerifyOTPEmailRegistrationRequest { } ]; // unique identifier of the OTP Email registration, which was returned in the add OTP Email. - string otp_email_id = 2 [ + string otp_email_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1938,7 +2042,7 @@ message VerifyOTPEmailRegistrationRequest { } ]; // Set the verification code generated during the add OTP Email request. - string code = 3 [ + string code = 5 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1950,12 +2054,19 @@ message VerifyOTPEmailRegistrationRequest { } message VerifyOTPEmailRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message RemoveOTPEmailAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1965,7 +2076,7 @@ message RemoveOTPEmailAuthenticatorRequest { } ]; // unique identifier of the OTP Email authenticator. - string otp_email_id = 2 [ + string otp_email_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1977,12 +2088,19 @@ message RemoveOTPEmailAuthenticatorRequest { } message RemoveOTPEmailAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message StartIdentityProviderIntentRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // ID of an existing identity provider (IDP). - string idp_id = 1 [ + string idp_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1993,13 +2111,13 @@ message StartIdentityProviderIntentRequest { ]; oneof content { - RedirectURLs urls = 2; - LDAPCredentials ldap = 3; + RedirectURLs urls = 4; + LDAPCredentials ldap = 5; } } message StartIdentityProviderIntentResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // the next step to take in the idp intent flow. oneof next_step { // The authentication URL to which the client should redirect. @@ -2016,9 +2134,16 @@ message StartIdentityProviderIntentResponse { } } -message RetrieveIdentityProviderIntentRequest { +message GetIdentityProviderIntentRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // ID of the identity provider (IDP) intent, previously returned on the success response of the start identity provider intent. - string idp_intent_id = 1 [ + string idp_intent_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2028,7 +2153,7 @@ message RetrieveIdentityProviderIntentRequest { } ]; // Token of the identity provider (IDP) intent, previously returned on the success response of the start identity provider intent. - string idp_intent_token = 2 [ + string idp_intent_token = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2039,8 +2164,8 @@ message RetrieveIdentityProviderIntentRequest { ]; } -message RetrieveIdentityProviderIntentResponse { - zitadel.object.v2.Details details = 1; +message GetIdentityProviderIntentResponse { + zitadel.resources.object.v3alpha.Details details = 1; // Information returned by the identity provider (IDP) such as the identification of the user // and detailed / profile information. IDPInformation idp_information = 2; @@ -2053,8 +2178,15 @@ message RetrieveIdentityProviderIntentResponse { } message AddIDPAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2063,16 +2195,23 @@ message AddIDPAuthenticatorRequest { example: "\"69629026806489455\""; } ]; - IDPAuthenticator idp_authenticator = 2; + IDPAuthenticator authenticator = 4; } message AddIDPAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message RemoveIDPAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2082,7 +2221,7 @@ message RemoveIDPAuthenticatorRequest { } ]; // unique identifier of the identity provider (IDP) authenticator. - string idp_id = 2 [ + string idp_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2094,6 +2233,6 @@ message RemoveIDPAuthenticatorRequest { } message RemoveIDPAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } diff --git a/proto/zitadel/user/schema/v3alpha/user_schema.proto b/proto/zitadel/resources/userschema/v3alpha/user_schema.proto similarity index 68% rename from proto/zitadel/user/schema/v3alpha/user_schema.proto rename to proto/zitadel/resources/userschema/v3alpha/user_schema.proto index e7a7a0737..cb957eadb 100644 --- a/proto/zitadel/user/schema/v3alpha/user_schema.proto +++ b/proto/zitadel/resources/userschema/v3alpha/user_schema.proto @@ -1,25 +1,18 @@ syntax = "proto3"; -package zitadel.user.schema.v3alpha; +package zitadel.resources.userschema.v3alpha; import "google/api/field_behavior.proto"; import "google/protobuf/struct.proto"; import "validate/validate.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; -import "zitadel/object/v2/object.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha;userschema"; message UserSchema { - - // ID is the read-only unique identifier of the schema. - string id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629012906488334\"" - } - ]; // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 2; + zitadel.resources.object.v3alpha.Details details = 2; // Type is a human readable text describing the schema. string type = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -60,55 +53,56 @@ enum FieldName { FIELD_NAME_STATE = 2; FIELD_NAME_REVISION = 3; FIELD_NAME_CHANGE_DATE = 4; + FIELD_NAME_CREATION_DATE = 5; } -message SearchQuery { - oneof query { +message SearchFilter { + oneof Filter { option (validate.required) = true; - // Union the results of each sub query ('OR'). - OrQuery or_query = 1; + // Union the results of each sub filter ('OR'). + OrFilter or_filter = 1; // Limit the result to match all sub queries ('AND'). // Note that if you specify multiple queries, they will be implicitly used as andQueries. - // Use the andQuery in combination with orQuery and notQuery. - AndQuery and_query = 2; - // Exclude / Negate the result of the sub query ('NOT'). - NotQuery not_query = 3; + // Use the andFilter in combination with orFilter and notFilter. + AndFilter and_filter = 2; + // Exclude / Negate the result of the sub filter ('NOT'). + NotFilter not_filter = 3; // Limit the result to a specific schema type. - TypeQuery type_query = 5; + TypeFilter type_filter = 5; // Limit the result to a specific state of the schema. - StateQuery state_query = 6; + StateFilter state_filter = 6; // Limit the result to a specific schema ID. - IDQuery id_query = 7; + IDFilter id_filter = 7; } } -message OrQuery { - repeated SearchQuery queries = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[{\"idQuery\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}},{\"idQuery\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}}]" +message OrFilter { + repeated SearchFilter queries = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"idFilter\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}},{\"idFilter\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}}]" } ]; } -message AndQuery { - repeated SearchQuery queries = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[{\"typeQuery\": {\"id\": \"employees\",\"method\": \"TEXT_QUERY_METHOD_STARTS_WITH\"}},{\"stateQuery\": {\"state\": \"STATE_ACTIVE\"}}]" +message AndFilter { + repeated SearchFilter queries = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"typeFilter\": {\"id\": \"employees\",\"method\": \"TEXT_FILTER_METHOD_STARTS_WITH\"}},{\"stateFilter\": {\"state\": \"STATE_ACTIVE\"}}]" } ]; } -message NotQuery { - SearchQuery query = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"stateQuery\": {\"state\": \"STATE_ACTIVE\"}}" +message NotFilter { + SearchFilter filter = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"stateFilter\": {\"state\": \"STATE_ACTIVE\"}}" } ]; } -message IDQuery { - // Defines the ID of the user schema to query for. +message IDFilter { + // Defines the ID of the user schema to filter for. string id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, @@ -118,14 +112,14 @@ message IDQuery { example: "\"163840776835432705\""; } ]; - // Defines which text comparison method used for the id query. - zitadel.object.v2.TextQueryMethod method = 2 [ + // Defines which text comparison method used for the id filter. + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } -message TypeQuery { - // Defines which type to query for. +message TypeFilter { + // Defines which type to filter for. string type = 1 [ (validate.rules).string = {max_len: 200}, (google.api.field_behavior) = REQUIRED, @@ -134,14 +128,14 @@ message TypeQuery { example: "\"employees\""; } ]; - // Defines which text comparison method used for the type query. - zitadel.object.v2.TextQueryMethod method = 2 [ + // Defines which text comparison method used for the type filter. + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } -message StateQuery { - // Defines the state to query for. +message StateFilter { + // Defines the state to filter for. State state = 1 [ (validate.rules).enum.defined_only = true, (google.api.field_behavior) = REQUIRED, diff --git a/proto/zitadel/user/schema/v3alpha/user_schema_service.proto b/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto similarity index 73% rename from proto/zitadel/user/schema/v3alpha/user_schema_service.proto rename to proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto index 14e59f1ea..77def934e 100644 --- a/proto/zitadel/user/schema/v3alpha/user_schema_service.proto +++ b/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package zitadel.user.schema.v3alpha; +package zitadel.resources.userschema.v3alpha; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; @@ -8,12 +8,12 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; +import "zitadel/object/v3alpha/object.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; -import "zitadel/user/schema/v3alpha/user_schema.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"; +import "zitadel/resources/userschema/v3alpha/user_schema.proto"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha;userschema"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { @@ -103,12 +103,12 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } }; -service UserSchemaService { +service ZITADELUserSchemas { - // List user schemas + // Search user schemas // - // List all matching user schemas. By default, we will return all user schema of your instance. Make sure to include a limit and sorting for pagination. - rpc ListUserSchemas (ListUserSchemasRequest) returns (ListUserSchemasResponse) { + // Search all matching user schemas. By default, we will return all user schema of your instance. Make sure to include a limit and sorting for pagination. + rpc SearchUserSchemas (SearchUserSchemasRequest) returns (SearchUserSchemasResponse) { option (google.api.http) = { post: "/v3alpha/user_schemas/search" body: "*" @@ -124,13 +124,13 @@ service UserSchemaService { responses: { key: "200"; value: { - description: "A list of all user schema matching the query"; + description: "A list of all user schema matching the search"; }; }; responses: { key: "400"; value: { - description: "invalid list query"; + description: "invalid search"; schema: { json_schema: { ref: "#/definitions/rpcStatus"; @@ -198,12 +198,12 @@ service UserSchemaService { }; } - // Update a user schema + // Patch a user schema // - // Update an existing user schema to a new revision. Users based on the current revision will not be affected until they are updated. - rpc UpdateUserSchema (UpdateUserSchemaRequest) returns (UpdateUserSchemaResponse) { + // Patch an existing user schema to a new revision. Users based on the current revision will not be affected until they are updated. + rpc PatchUserSchema (PatchUserSchemaRequest) returns (PatchUserSchemaResponse) { option (google.api.http) = { - put: "/v3alpha/user_schemas/{id}" + patch: "/v3alpha/user_schemas/{id}" body: "*" }; @@ -297,26 +297,31 @@ service UserSchemaService { } -message ListUserSchemasRequest { - // list limitations and ordering. - zitadel.object.v2.ListQuery query = 1; - // the field the result is sorted. - zitadel.user.schema.v3alpha.FieldName sorting_column = 2 [ +message SearchUserSchemasRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"FIELD_NAME_TYPE\""; + default: "\"domain from HOST or :authority header\"" } ]; - // Define the criteria to query for. - repeated zitadel.user.schema.v3alpha.SearchQuery queries = 3; + // list limitations and ordering. + optional zitadel.resources.object.v3alpha.SearchQuery query = 2; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional FieldName sorting_column = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"TARGET_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to filter for. + repeated SearchFilter filters = 4; } -message ListUserSchemasResponse { +message SearchUserSchemasResponse { // Details provides information about the returned result including total amount found. - zitadel.object.v2.ListDetails details = 1; + zitadel.resources.object.v3alpha.ListDetails details = 1; // States by which field the results are sorted. - zitadel.user.schema.v3alpha.FieldName sorting_column = 2; + FieldName sorting_column = 2; // The result contains the user schemas, which matched the queries. - repeated zitadel.user.schema.v3alpha.UserSchema result = 3; + repeated UserSchema result = 3; } @@ -334,11 +339,29 @@ message GetUserSchemaByIDRequest { } message GetUserSchemaByIDResponse { - zitadel.user.schema.v3alpha.UserSchema schema = 1; + zitadel.resources.userschema.v3alpha.UserSchema schema = 1; } - message CreateUserSchemaRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + CreateUserSchema user_schema = 2 [ + (validate.rules).message = { + required: true + } + ]; +} + +message CreateUserSchema{ + optional zitadel.object.v3alpha.Instance instance = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Type is a human readable word describing the schema. string type = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -373,14 +396,17 @@ message CreateUserSchemaRequest { } message CreateUserSchemaResponse { - // ID is the read-only unique identifier of the schema. - string id = 1; // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 2; + zitadel.resources.object.v3alpha.Details details = 2; } -message UpdateUserSchemaRequest { +message PatchUserSchemaRequest { + optional zitadel.object.v3alpha.Instance instance = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; // unique identifier of the schema. string id = 1; // Type is a human readable word describing the schema. @@ -414,39 +440,75 @@ message UpdateUserSchemaRequest { ]; } -message UpdateUserSchemaResponse { +message PatchUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message DeactivateUserSchemaRequest { + optional zitadel.object.v3alpha.Instance instance = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; // unique identifier of the schema. - string id = 1; + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; } message DeactivateUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message ReactivateUserSchemaRequest { + optional zitadel.object.v3alpha.Instance instance = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; // unique identifier of the schema. - string id = 1; + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; } message ReactivateUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message DeleteUserSchemaRequest { + optional zitadel.object.v3alpha.Instance instance = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; // unique identifier of the schema. - string id = 1; + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; } message DeleteUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } diff --git a/proto/zitadel/user/v3alpha/user.proto b/proto/zitadel/user/v3alpha/user.proto deleted file mode 100644 index 47b9d7ee9..000000000 --- a/proto/zitadel/user/v3alpha/user.proto +++ /dev/null @@ -1,66 +0,0 @@ -syntax = "proto3"; - -package zitadel.user.v3alpha; - -import "google/api/field_behavior.proto"; -import "google/protobuf/struct.proto"; -import "google/protobuf/timestamp.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; -import "zitadel/user/v3alpha/authenticator.proto"; -import "zitadel/user/v3alpha/communication.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha"; - -message User { - - // ID is the read-only unique identifier of the user. - string user_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629012906488334\""; - } - ]; - // Details provide some base information (such as the last change date) of the user. - zitadel.object.v2.Details details = 2; - // The user's authenticators. They are used to identify and authenticate the user - // during the authentication process. - Authenticators authenticators = 3; - // Contact information for the user. ZITADEL will use this in case of internal notifications. - Contact contact = 4; - // State of the user. - State state = 5; - // The schema the user and it's data is based on. - Schema schema = 6; - // The user's data based on the provided schema. - google.protobuf.Struct data = 7; -} - -enum State { - USER_STATE_UNSPECIFIED = 0; - USER_STATE_ACTIVE = 1; - USER_STATE_INACTIVE = 2; - USER_STATE_DELETED = 3; - USER_STATE_LOCKED = 4; -} - -message Schema { - // The unique identifier of the user schema. - string id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629026806489455\"" - } - ]; - // The human readable name of the user schema. - string type = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"employees\""; - } - ]; - // The revision the user's data is based on of the revision. - uint32 revision = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "7"; - } - ]; -}