From 7494a7b6d9879b4f283660a658db88ad0c971d12 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 22 Mar 2024 14:26:13 +0100 Subject: [PATCH] feat(api): add possibility to retrieve user schemas (#7614) This PR extends the user schema service (V3 API) with the possibility to ListUserSchemas and GetUserSchemaByID. The previously started guide is extended to demonstrate how to retrieve the schema(s) and notes the generated revision property. --- cmd/defaults.yaml | 2 + .../guides/manage/customize/user-schema.md | 96 ++++++ internal/api/grpc/user/query.go | 2 +- .../api/grpc/user/schema/v3alpha/schema.go | 236 ++++++++++++++ .../schema/v3alpha/schema_integration_test.go | 292 ++++++++++++++++++ internal/api/grpc/user/v2/query.go | 2 +- internal/eventstore/handler/v2/statement.go | 11 + internal/integration/client.go | 6 +- internal/query/prepare_test.go | 4 +- internal/query/projection/projection.go | 3 + internal/query/projection/user_schema.go | 203 ++++++++++++ internal/query/projection/user_schema_test.go | 219 +++++++++++++ internal/query/user_schema.go | 218 +++++++++++++ internal/query/user_schema_test.go | 290 +++++++++++++++++ internal/static/i18n/bg.yaml | 2 +- internal/static/i18n/cs.yaml | 1 + internal/static/i18n/de.yaml | 2 +- internal/static/i18n/en.yaml | 2 +- internal/static/i18n/es.yaml | 2 +- internal/static/i18n/fr.yaml | 2 +- internal/static/i18n/it.yaml | 2 +- internal/static/i18n/ja.yaml | 2 +- internal/static/i18n/mk.yaml | 2 +- internal/static/i18n/nl.yaml | 2 +- internal/static/i18n/pl.yaml | 2 +- internal/static/i18n/pt.yaml | 2 +- internal/static/i18n/ru.yaml | 1 + internal/static/i18n/zh.yaml | 2 +- .../user/schema/v3alpha/user_schema.proto | 6 +- 29 files changed, 1597 insertions(+), 19 deletions(-) create mode 100644 internal/query/projection/user_schema.go create mode 100644 internal/query/projection/user_schema_test.go create mode 100644 internal/query/user_schema.go create mode 100644 internal/query/user_schema_test.go diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 5db2d941a..885a166d9 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -1017,6 +1017,7 @@ InternalAuthZ: - "execution.read" - "execution.write" - "execution.delete" + - "userschema.read" - "userschema.write" - "userschema.delete" - Role: "IAM_OWNER_VIEWER" @@ -1051,6 +1052,7 @@ InternalAuthZ: - "milestones.read" - "execution.target.read" - "execution.read" + - "userschema.read" - Role: "IAM_ORG_MANAGER" Permissions: - "org.read" diff --git a/docs/docs/guides/manage/customize/user-schema.md b/docs/docs/guides/manage/customize/user-schema.md index ceab18873..5915e24dd 100644 --- a/docs/docs/guides/manage/customize/user-schema.md +++ b/docs/docs/guides/manage/customize/user-schema.md @@ -131,5 +131,101 @@ curl -X PUT "https://$CUSTOM-DOMAIN/v3alpha/user_schemas/$SCHEMA_ID" \ } } } +}' +``` + +## Retrieve the Existing Schemas + +To check the state of existing schemas you can simply [list them](/apis/resources/user_schema_service_v3/user-schema-service-list-user-schemas). +In this case we will query for the one with state `active`. Check out the api documentation for detailed information on possible filters. +The API also allows to retrieve a single [schema by ID](/apis/resources/user_schema_service_v3/user-schema-service-get-user-schema-by-id). + +```bash +curl -X POST "https://$CUSTOM-DOMAIN/v3alpha/user_schemas/search" \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H "Authorization: Bearer $ACCESS_TOKEN" \ +--data-raw '{ + "query": { + "offset": "0", + "limit": 100, + "asc": true + }, + "sortingColumn": "FIELD_NAME_TYPE", + "queries": [ + { + "stateQuery": { + "state": "STATE_ACTIVE" + } + } + ] +}' +``` + +If you've followed this guide, it should list you a singe schema: + +```json +{ + "details": { + "totalResult": "1", + "timestamp": "2024-03-21T16:35:19.685700Z" + }, + "result": [ + { + "id": "259279890237358500", + "details": { + "sequence": "2", + "changeDate": "2024-03-21T16:35:19.685700Z", + "resourceOwner": "224313188550750765" + }, + "type": "user", + "state": "STATE_ACTIVE", + "revision": 2, + "schema": { + "$schema": "urn:zitadel:schema:v1", + "properties": { + "customerId": { + "type": "string", + "urn:zitadel:schema:permission": { + "owner": "rw" + } + }, + "familyName": { + "type": "string", + "urn:zitadel:schema:permission": { + "owner": "rw", + "self": "rw" + } + }, + "givenName": { + "type": "string", + "urn:zitadel:schema:permission": { + "owner": "rw", + "self": "rw" + } + }, + "profileUri": { + "format": "uri", + "type": "string", + "urn:zitadel:schema:permission": { + "owner": "rw", + "self": "r" + } + } + }, + "type": "object" + }, + "possibleAuthenticators": [ + "AUTHENTICATOR_TYPE_USERNAME", + "AUTHENTICATOR_TYPE_PASSWORD" + ] + } + ] } ``` + +### Revision + +Note the `revision` property, which is currently `2`. Each update to the `schema`-property will increase +it by `1`. The revision will later be reflected on the managed users to state based on which revision of the schema +they were last updated on. \ No newline at end of file diff --git a/internal/api/grpc/user/query.go b/internal/api/grpc/user/query.go index 7cd3d3799..41cce01a8 100644 --- a/internal/api/grpc/user/query.go +++ b/internal/api/grpc/user/query.go @@ -21,7 +21,7 @@ func UserQueriesToQuery(queries []*user_pb.SearchQuery, level uint8) (_ []query. func UserQueryToQuery(query *user_pb.SearchQuery, level uint8) (query.SearchQuery, error) { if level > 20 { // can't go deeper than 20 levels of nesting. - return nil, zerrors.ThrowInvalidArgument(nil, "USER-zsQ97", "Errors.User.TooManyNestingLevels") + return nil, zerrors.ThrowInvalidArgument(nil, "USER-zsQ97", "Errors.Query.TooManyNestingLevels") } switch q := query.Query.(type) { case *user_pb.SearchQuery_UserNameQuery: diff --git a/internal/api/grpc/user/schema/v3alpha/schema.go b/internal/api/grpc/user/schema/v3alpha/schema.go index 29dd67048..73342f702 100644 --- a/internal/api/grpc/user/schema/v3alpha/schema.go +++ b/internal/api/grpc/user/schema/v3alpha/schema.go @@ -3,10 +3,13 @@ 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" ) @@ -45,6 +48,7 @@ func (s *Server) UpdateUserSchema(ctx context.Context, req *schema.UpdateUserSch 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 @@ -57,6 +61,7 @@ func (s *Server) DeactivateUserSchema(ctx context.Context, req *schema.Deactivat 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 @@ -69,6 +74,7 @@ func (s *Server) ReactivateUserSchema(ctx context.Context, req *schema.Reactivat 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 @@ -82,6 +88,153 @@ func (s *Server) DeleteUserSchema(ctx context.Context, req *schema.DeleteUserSch }, 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 @@ -148,3 +301,86 @@ func authenticatorTypeToDomain(authenticator schema.AuthenticatorType) domain.Au 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 index 177b9c813..bbd320065 100644 --- a/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go +++ b/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go @@ -15,6 +15,7 @@ import ( "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" feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" @@ -810,3 +811,294 @@ func TestServer_DeleteUserSchema(t *testing.T) { }) } } + +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/v2/query.go b/internal/api/grpc/user/v2/query.go index 225b46930..32cb2775f 100644 --- a/internal/api/grpc/user/v2/query.go +++ b/internal/api/grpc/user/v2/query.go @@ -218,7 +218,7 @@ func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.Sea func userQueryToQuery(query *user.SearchQuery, level uint8) (query.SearchQuery, error) { if level > 20 { // can't go deeper than 20 levels of nesting. - return nil, zerrors.ThrowInvalidArgument(nil, "USER-zsQ97", "Errors.User.TooManyNestingLevels") + return nil, zerrors.ThrowInvalidArgument(nil, "USER-zsQ97", "Errors.Query.TooManyNestingLevels") } switch q := query.Query.(type) { case *user.SearchQuery_UserNameQuery: diff --git a/internal/eventstore/handler/v2/statement.go b/internal/eventstore/handler/v2/statement.go index 8d2898629..802bacc06 100644 --- a/internal/eventstore/handler/v2/statement.go +++ b/internal/eventstore/handler/v2/statement.go @@ -10,6 +10,7 @@ import ( "time" "github.com/zitadel/logging" + "golang.org/x/exp/constraints" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" @@ -504,6 +505,16 @@ func NewJSONCol(name string, value interface{}) Column { return NewCol(name, marshalled) } +func NewIncrementCol[Int constraints.Integer](column string, value Int) Column { + return Column{ + Name: column, + Value: value, + ParameterOpt: func(placeholder string) string { + return column + " + " + placeholder + }, + } +} + type Condition func(param string) (string, []any) type NamespacedCondition func(namespace string) Condition diff --git a/internal/integration/client.go b/internal/integration/client.go index 6aa89d94e..257759b38 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -570,6 +570,10 @@ func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *execution } 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) CreateUserSchemaWithType(ctx context.Context, t *testing.T, schemaType string) *schema.CreateUserSchemaResponse { userSchema := new(structpb.Struct) err := userSchema.UnmarshalJSON([]byte(`{ "$schema": "urn:zitadel:schema:v1", @@ -578,7 +582,7 @@ func (s *Tester) CreateUserSchema(ctx context.Context, t *testing.T) *schema.Cre }`)) require.NoError(t, err) target, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), + Type: schemaType, DataType: &schema.CreateUserSchemaRequest_Schema{ Schema: userSchema, }, diff --git a/internal/query/prepare_test.go b/internal/query/prepare_test.go index dadf95f00..cea2d9535 100644 --- a/internal/query/prepare_test.go +++ b/internal/query/prepare_test.go @@ -265,8 +265,8 @@ func validatePrepare(prepareType reflect.Type) error { if prepareType.Kind() != reflect.Func { return errors.New("prepare is not a function") } - if prepareType.NumIn() < 2 { - return fmt.Errorf("prepare: invalid number of inputs: want: 0 got %d", prepareType.NumIn()) + if prepareType.NumIn() != 0 && prepareType.NumIn() != 2 { + return fmt.Errorf("prepare: invalid number of inputs: want: 0 or 2 got %d", prepareType.NumIn()) } if prepareType.NumOut() != 2 { return fmt.Errorf("prepare: invalid number of outputs: want: 2 got %d", prepareType.NumOut()) diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index f6afefda2..0f8b2518a 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -76,6 +76,7 @@ var ( InstanceFeatureProjection *handler.Handler TargetProjection *handler.Handler ExecutionProjection *handler.Handler + UserSchemaProjection *handler.Handler ) type projection interface { @@ -156,6 +157,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, InstanceFeatureProjection = newInstanceFeatureProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_features"])) TargetProjection = newTargetProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["targets"])) ExecutionProjection = newExecutionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["executions"])) + UserSchemaProjection = newUserSchemaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_schemas"])) newProjectionsList() return nil } @@ -269,5 +271,6 @@ func newProjectionsList() { InstanceFeatureProjection, ExecutionProjection, TargetProjection, + UserSchemaProjection, } } diff --git a/internal/query/projection/user_schema.go b/internal/query/projection/user_schema.go new file mode 100644 index 000000000..2961596e3 --- /dev/null +++ b/internal/query/projection/user_schema.go @@ -0,0 +1,203 @@ +package projection + +import ( + "context" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/user/schema" +) + +const ( + UserSchemaTable = "projections.user_schemas" + + UserSchemaIDCol = "id" + UserSchemaChangeDateCol = "change_date" + UserSchemaSequenceCol = "sequence" + UserSchemaInstanceIDCol = "instance_id" + UserSchemaStateCol = "state" + UserSchemaTypeCol = "type" + UserSchemaRevisionCol = "revision" + UserSchemaSchemaCol = "schema" + UserSchemaPossibleAuthenticatorsCol = "possible_authenticators" +) + +type userSchemaProjection struct{} + +func newUserSchemaProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(userSchemaProjection)) +} + +func (*userSchemaProjection) Name() string { + return UserSchemaTable +} + +func (*userSchemaProjection) Init() *old_handler.Check { + return handler.NewTableCheck( + handler.NewTable([]*handler.InitColumn{ + handler.NewColumn(UserSchemaIDCol, handler.ColumnTypeText), + handler.NewColumn(UserSchemaChangeDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(UserSchemaSequenceCol, handler.ColumnTypeInt64), + handler.NewColumn(UserSchemaStateCol, handler.ColumnTypeEnum), + handler.NewColumn(UserSchemaInstanceIDCol, handler.ColumnTypeText), + handler.NewColumn(UserSchemaTypeCol, handler.ColumnTypeText), + handler.NewColumn(UserSchemaRevisionCol, handler.ColumnTypeInt64), + handler.NewColumn(UserSchemaSchemaCol, handler.ColumnTypeJSONB, handler.Nullable()), + handler.NewColumn(UserSchemaPossibleAuthenticatorsCol, handler.ColumnTypeEnumArray, handler.Nullable()), + }, + handler.NewPrimaryKey(UserSchemaInstanceIDCol, UserSchemaIDCol), + ), + ) +} + +func (p *userSchemaProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: schema.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: schema.CreatedType, + Reduce: p.reduceCreated, + }, + { + Event: schema.UpdatedType, + Reduce: p.reduceUpdated, + }, + { + Event: schema.DeactivatedType, + Reduce: p.reduceDeactivated, + }, + { + Event: schema.ReactivatedType, + Reduce: p.reduceReactivated, + }, + { + Event: schema.DeletedType, + Reduce: p.reduceDeleted, + }, + }, + }, + { + Aggregate: instance.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: instance.InstanceRemovedEventType, + Reduce: reduceInstanceRemovedHelper(UserSchemaInstanceIDCol), + }, + }, + }, + } +} + +func (p *userSchemaProjection) reduceCreated(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*schema.CreatedEvent](event) + if err != nil { + return nil, err + } + + return handler.NewCreateStatement( + event, + []handler.Column{ + handler.NewCol(UserSchemaIDCol, event.Aggregate().ID), + handler.NewCol(UserSchemaChangeDateCol, event.CreatedAt()), + handler.NewCol(UserSchemaSequenceCol, event.Sequence()), + handler.NewCol(UserSchemaInstanceIDCol, event.Aggregate().InstanceID), + handler.NewCol(UserSchemaStateCol, domain.UserSchemaStateActive), + handler.NewCol(UserSchemaTypeCol, e.SchemaType), + handler.NewCol(UserSchemaRevisionCol, 1), + handler.NewCol(UserSchemaSchemaCol, e.Schema), + handler.NewCol(UserSchemaPossibleAuthenticatorsCol, e.PossibleAuthenticators), + }, + ), nil +} + +func (p *userSchemaProjection) reduceUpdated(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*schema.UpdatedEvent](event) + if err != nil { + return nil, err + } + + cols := []handler.Column{ + handler.NewCol(UserSchemaChangeDateCol, event.CreatedAt()), + handler.NewCol(UserSchemaSequenceCol, event.Sequence()), + } + if e.SchemaType != nil { + cols = append(cols, handler.NewCol(UserSchemaTypeCol, *e.SchemaType)) + } + + if len(e.Schema) > 0 { + cols = append(cols, handler.NewCol(UserSchemaSchemaCol, e.Schema)) + cols = append(cols, handler.NewIncrementCol(UserSchemaRevisionCol, 1)) + } + + if len(e.PossibleAuthenticators) > 0 { + cols = append(cols, handler.NewCol(UserSchemaPossibleAuthenticatorsCol, e.PossibleAuthenticators)) + } + + return handler.NewUpdateStatement( + event, + cols, + []handler.Condition{ + handler.NewCond(UserSchemaIDCol, event.Aggregate().ID), + handler.NewCond(UserSchemaInstanceIDCol, event.Aggregate().InstanceID), + }, + ), nil +} + +func (p *userSchemaProjection) reduceDeactivated(event eventstore.Event) (*handler.Statement, error) { + _, err := assertEvent[*schema.DeactivatedEvent](event) + if err != nil { + return nil, err + } + + return handler.NewUpdateStatement( + event, + []handler.Column{ + handler.NewCol(UserSchemaChangeDateCol, event.CreatedAt()), + handler.NewCol(UserSchemaSequenceCol, event.Sequence()), + handler.NewCol(UserSchemaStateCol, domain.UserSchemaStateInactive), + }, + []handler.Condition{ + handler.NewCond(UserSchemaIDCol, event.Aggregate().ID), + handler.NewCond(UserSchemaInstanceIDCol, event.Aggregate().InstanceID), + }, + ), nil +} + +func (p *userSchemaProjection) reduceReactivated(event eventstore.Event) (*handler.Statement, error) { + _, err := assertEvent[*schema.ReactivatedEvent](event) + if err != nil { + return nil, err + } + + return handler.NewUpdateStatement( + event, + []handler.Column{ + handler.NewCol(UserSchemaChangeDateCol, event.CreatedAt()), + handler.NewCol(UserSchemaSequenceCol, event.Sequence()), + handler.NewCol(UserSchemaStateCol, domain.UserSchemaStateActive), + }, + []handler.Condition{ + handler.NewCond(UserSchemaIDCol, event.Aggregate().ID), + handler.NewCond(UserSchemaInstanceIDCol, event.Aggregate().InstanceID), + }, + ), nil +} + +func (p *userSchemaProjection) reduceDeleted(event eventstore.Event) (*handler.Statement, error) { + _, err := assertEvent[*schema.DeletedEvent](event) + if err != nil { + return nil, err + } + + return handler.NewDeleteStatement( + event, + []handler.Condition{ + handler.NewCond(UserSchemaIDCol, event.Aggregate().ID), + handler.NewCond(UserSchemaInstanceIDCol, event.Aggregate().InstanceID), + }, + ), nil +} diff --git a/internal/query/projection/user_schema_test.go b/internal/query/projection/user_schema_test.go new file mode 100644 index 000000000..ac7b0704c --- /dev/null +++ b/internal/query/projection/user_schema_test.go @@ -0,0 +1,219 @@ +package projection + +import ( + "encoding/json" + "testing" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/user/schema" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestUserSchemaProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "reduceCreated", + args: args{ + event: getEvent( + testEvent( + schema.CreatedType, + 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]}`), + ), eventstore.GenericEventMapper[schema.CreatedEvent]), + }, + reduce: (&userSchemaProjection{}).reduceCreated, + want: wantReduce{ + aggregateType: eventstore.AggregateType("user_schema"), + sequence: 15, + 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)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + uint64(15), + "instance-id", + domain.UserSchemaStateActive, + "type", + 1, + json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, + }, + }, + }, + }, + }, + }, + { + name: "reduceUpdated", + args: args{ + event: getEvent( + testEvent( + schema.CreatedType, + 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]}`), + ), eventstore.GenericEventMapper[schema.UpdatedEvent]), + }, + reduce: (&userSchemaProjection{}).reduceUpdated, + want: wantReduce{ + aggregateType: eventstore.AggregateType("user_schema"), + sequence: 15, + 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)", + 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, + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, + "agg-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceDeactivated", + args: args{ + event: getEvent( + testEvent( + schema.DeactivatedType, + schema.AggregateType, + nil, + ), eventstore.GenericEventMapper[schema.DeactivatedEvent]), + }, + reduce: (&userSchemaProjection{}).reduceDeactivated, + want: wantReduce{ + aggregateType: eventstore.AggregateType("user_schema"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + domain.UserSchemaStateInactive, + "agg-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceReactivated", + args: args{ + event: getEvent( + testEvent( + schema.ReactivatedType, + schema.AggregateType, + nil, + ), eventstore.GenericEventMapper[schema.ReactivatedEvent]), + }, + reduce: (&userSchemaProjection{}).reduceReactivated, + want: wantReduce{ + aggregateType: eventstore.AggregateType("user_schema"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + domain.UserSchemaStateActive, + "agg-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceDeleted", + args: args{ + event: getEvent( + testEvent( + schema.DeletedType, + schema.AggregateType, + nil, + ), eventstore.GenericEventMapper[schema.DeletedEvent]), + }, + reduce: (&userSchemaProjection{}).reduceDeleted, + want: wantReduce{ + aggregateType: eventstore.AggregateType("user_schema"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.user_schemas WHERE (id = $1) AND (instance_id = $2)", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceInstanceRemoved", + args: args{ + event: getEvent( + testEvent( + instance.InstanceRemovedEventType, + instance.AggregateType, + nil, + ), instance.InstanceRemovedEventMapper), + }, + reduce: reduceInstanceRemovedHelper(UserSchemaInstanceIDCol), + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.user_schemas WHERE (instance_id = $1)", + expectedArgs: []interface{}{ + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if ok := zerrors.IsErrorInvalidArgument(err); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, UserSchemaTable, tt.want) + }) + } +} diff --git a/internal/query/user_schema.go b/internal/query/user_schema.go new file mode 100644 index 000000000..6b51fa6a0 --- /dev/null +++ b/internal/query/user_schema.go @@ -0,0 +1,218 @@ +package query + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type UserSchemas struct { + SearchResponse + UserSchemas []*UserSchema +} + +func (e *UserSchemas) SetState(s *State) { + e.State = s +} + +type UserSchema struct { + ID string + domain.ObjectDetails + State domain.UserSchemaState + Type string + Revision uint32 + Schema json.RawMessage + PossibleAuthenticators database.Array[domain.AuthenticatorType] +} + +type UserSchemaSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +var ( + userSchemaTable = table{ + name: projection.UserSchemaTable, + instanceIDCol: projection.UserSchemaInstanceIDCol, + } + UserSchemaIDCol = Column{ + name: projection.UserSchemaIDCol, + table: userSchemaTable, + } + UserSchemaChangeDateCol = Column{ + name: projection.UserSchemaChangeDateCol, + table: userSchemaTable, + } + UserSchemaInstanceIDCol = Column{ + name: projection.UserSchemaInstanceIDCol, + table: userSchemaTable, + } + UserSchemaSequenceCol = Column{ + name: projection.UserSchemaSequenceCol, + table: userSchemaTable, + } + UserSchemaStateCol = Column{ + name: projection.UserSchemaStateCol, + table: userSchemaTable, + } + UserSchemaTypeCol = Column{ + name: projection.UserSchemaTypeCol, + table: userSchemaTable, + } + UserSchemaRevisionCol = Column{ + name: projection.UserSchemaRevisionCol, + table: userSchemaTable, + } + UserSchemaSchemaCol = Column{ + name: projection.UserSchemaSchemaCol, + table: userSchemaTable, + } + UserSchemaPossibleAuthenticatorsCol = Column{ + name: projection.UserSchemaPossibleAuthenticatorsCol, + table: userSchemaTable, + } +) + +func (q *Queries) GetUserSchemaByID(ctx context.Context, id string) (userSchema *UserSchema, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + eq := sq.Eq{ + UserSchemaIDCol.identifier(): id, + UserSchemaInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), + } + + query, scan := prepareUserSchemaQuery() + return genericRowQuery[*UserSchema](ctx, q.client, query.Where(eq), scan) +} + +func (q *Queries) SearchUserSchema(ctx context.Context, queries *UserSchemaSearchQueries) (userSchemas *UserSchemas, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + eq := sq.Eq{ + UserSchemaInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), + } + + query, scan := prepareUserSchemasQuery() + return genericRowsQueryWithState[*UserSchemas](ctx, q.client, userSchemaTable, combineToWhereStmt(query, queries.toQuery, eq), scan) +} + +func (q *UserSchemaSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func NewUserSchemaTypeSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(UserSchemaTypeCol, value, comparison) +} + +func NewUserSchemaIDSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(UserSchemaIDCol, value, comparison) +} + +func NewUserSchemaStateSearchQuery(value domain.UserSchemaState) (SearchQuery, error) { + return NewNumberQuery(UserSchemaStateCol, value, NumberEquals) +} + +func prepareUserSchemaQuery() (sq.SelectBuilder, func(*sql.Row) (*UserSchema, error)) { + return sq.Select( + UserSchemaIDCol.identifier(), + UserSchemaChangeDateCol.identifier(), + UserSchemaSequenceCol.identifier(), + UserSchemaInstanceIDCol.identifier(), + UserSchemaStateCol.identifier(), + UserSchemaTypeCol.identifier(), + UserSchemaRevisionCol.identifier(), + UserSchemaSchemaCol.identifier(), + UserSchemaPossibleAuthenticatorsCol.identifier(), + ). + From(userSchemaTable.identifier()). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*UserSchema, error) { + u := new(UserSchema) + err := row.Scan( + &u.ID, + &u.EventDate, + &u.Sequence, + &u.ResourceOwner, + &u.State, + &u.Type, + &u.Revision, + &u.Schema, + &u.PossibleAuthenticators, + ) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-SAF3t", "Errors.Metadata.NotFound") + } + return nil, zerrors.ThrowInternal(err, "QUERY-WRB2Q", "Errors.Internal") + } + return u, nil + } +} + +func prepareUserSchemasQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserSchemas, error)) { + return sq.Select( + UserSchemaIDCol.identifier(), + UserSchemaChangeDateCol.identifier(), + UserSchemaSequenceCol.identifier(), + UserSchemaInstanceIDCol.identifier(), + UserSchemaStateCol.identifier(), + UserSchemaTypeCol.identifier(), + UserSchemaRevisionCol.identifier(), + UserSchemaSchemaCol.identifier(), + UserSchemaPossibleAuthenticatorsCol.identifier(), + countColumn.identifier()). + From(userSchemaTable.identifier()). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*UserSchemas, error) { + schema := make([]*UserSchema, 0) + var count uint64 + for rows.Next() { + u := new(UserSchema) + err := rows.Scan( + &u.ID, + &u.EventDate, + &u.Sequence, + &u.ResourceOwner, + &u.State, + &u.Type, + &u.Revision, + &u.Schema, + &u.PossibleAuthenticators, + &count, + ) + if err != nil { + return nil, err + } + + schema = append(schema, u) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-Lwj2e", "Errors.Query.CloseRows") + } + + return &UserSchemas{ + UserSchemas: schema, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} diff --git a/internal/query/user_schema_test.go b/internal/query/user_schema_test.go new file mode 100644 index 000000000..badcd1366 --- /dev/null +++ b/internal/query/user_schema_test.go @@ -0,0 +1,290 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" +) + +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,` + + ` COUNT(*) OVER ()` + + ` FROM projections.user_schemas` + prepareUserSchemasCols = []string{ + "id", + "change_date", + "sequence", + "instance_id", + "state", + "type", + "revision", + "schema", + "possible_authenticators", + "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` + + ` FROM projections.user_schemas` + prepareUserSchemaCols = []string{ + "id", + "change_date", + "sequence", + "instance_id", + "state", + "type", + "revision", + "schema", + "possible_authenticators", + } +) + +func Test_UserSchemaPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareUserSchemasQuery no result", + prepare: prepareUserSchemasQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareUserSchemasStmt), + nil, + nil, + ), + }, + object: &UserSchemas{UserSchemas: []*UserSchema{}}, + }, + { + name: "prepareUserSchemasQuery one result", + prepare: prepareUserSchemasQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareUserSchemasStmt), + prepareUserSchemasCols, + [][]driver.Value{ + { + "id", + testNow, + uint64(20211109), + "instance-id", + domain.UserSchemaStateActive, + "type", + 1, + json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`), + database.Array[domain.AuthenticatorType]{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, + }, + }, + ), + }, + object: &UserSchemas{ + SearchResponse: SearchResponse{ + Count: 1, + }, + UserSchemas: []*UserSchema{ + { + ID: "id", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + Sequence: 20211109, + ResourceOwner: "instance-id", + }, + State: domain.UserSchemaStateActive, + Type: "type", + Revision: 1, + Schema: json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`), + PossibleAuthenticators: database.Array[domain.AuthenticatorType]{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, + }, + }, + }, + }, + { + name: "prepareUserSchemasQuery multiple result", + prepare: prepareUserSchemasQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareUserSchemasStmt), + prepareUserSchemasCols, + [][]driver.Value{ + { + "id-1", + testNow, + uint64(20211109), + "instance-id", + domain.UserSchemaStateActive, + "type1", + 1, + json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`), + database.Array[domain.AuthenticatorType]{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, + }, + { + "id-2", + testNow, + uint64(20211110), + "instance-id", + domain.UserSchemaStateInactive, + "type2", + 2, + json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`), + database.Array[domain.AuthenticatorType]{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, + }, + }, + ), + }, + object: &UserSchemas{ + SearchResponse: SearchResponse{ + Count: 2, + }, + UserSchemas: []*UserSchema{ + { + ID: "id-1", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + Sequence: 20211109, + ResourceOwner: "instance-id", + }, + State: domain.UserSchemaStateActive, + Type: "type1", + Revision: 1, + Schema: json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`), + PossibleAuthenticators: database.Array[domain.AuthenticatorType]{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, + }, + { + ID: "id-2", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + Sequence: 20211110, + ResourceOwner: "instance-id", + }, + State: domain.UserSchemaStateInactive, + Type: "type2", + Revision: 2, + Schema: json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`), + PossibleAuthenticators: database.Array[domain.AuthenticatorType]{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, + }, + }, + }, + }, + { + name: "prepareUserSchemasQuery sql err", + prepare: prepareUserSchemasQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(prepareUserSchemasStmt), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: (*UserSchema)(nil), + }, + { + name: "prepareUserSchemaQuery no result", + prepare: prepareUserSchemaQuery, + want: want{ + sqlExpectations: mockQueriesScanErr( + regexp.QuoteMeta(prepareUserSchemaStmt), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !zerrors.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*UserSchema)(nil), + }, + { + name: "prepareUserSchemaQuery found", + prepare: prepareUserSchemaQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(prepareUserSchemaStmt), + prepareUserSchemaCols, + []driver.Value{ + "id", + testNow, + uint64(20211109), + "instance-id", + domain.UserSchemaStateActive, + "type", + 1, + json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`), + database.Array[domain.AuthenticatorType]{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, + }, + ), + }, + object: &UserSchema{ + ID: "id", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + Sequence: 20211109, + ResourceOwner: "instance-id", + }, + State: domain.UserSchemaStateActive, + Type: "type", + Revision: 1, + Schema: json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`), + PossibleAuthenticators: database.Array[domain.AuthenticatorType]{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, + }, + }, + { + name: "prepareUserSchemaQuery sql err", + prepare: prepareUserSchemaQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(prepareUserSchemaStmt), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: (*UserSchema)(nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index c2f66bd7b..b8ae47069 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -62,7 +62,6 @@ Errors: Notification: NoDomain: Няма намерен домейн за съобщение User: - TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: Потребителят не може да бъде намерен AlreadyExists: Вече съществува потребител NotFoundOnOrg: Потребителят не може да бъде намерен в избраната организация @@ -493,6 +492,7 @@ Errors: CloseRows: SQL изразът не можа да бъде завършен SQLStatement: SQL изразът не може да бъде създаден InvalidRequest: Заявката е невалидна + TooManyNestingLevels: Твърде много нива на влагане на заявката (макс. 20) Quota: AlreadyExists: Вече съществува квота за тази единица NotFound: Не е намерена квота за тази единица diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 2c9cef25b..604678371 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -478,6 +478,7 @@ Errors: CloseRows: SQL příkaz nemohl být dokončen SQLStatement: SQL příkaz nemohl být vytvořen InvalidRequest: Požadavek je neplatný + TooManyNestingLevels: Příliš mnoho úrovní vnoření dotazů (max. 20) Quota: AlreadyExists: Kvóta pro tuto jednotku již existuje NotFound: Kvóta pro tuto jednotku nenalezena diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 83251fc34..09241c3f1 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -60,7 +60,6 @@ Errors: Notification: NoDomain: Keine Domäne für Nachricht gefunden User: - TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: Benutzer konnte nicht gefunden werden AlreadyExists: Benutzer existiert bereits NotFoundOnOrg: Benutzer konnte in der gewünschten Organisation nicht gefunden werden @@ -479,6 +478,7 @@ Errors: CloseRows: SQL Statement konnte nicht abgeschlossen werden SQLStatement: SQL Statement konnte nicht erstellt werden InvalidRequest: Anfrage ist ungültig + TooManyNestingLevels: Zu viele Abfrageverschachtelungsebenen (maximal 20) Quota: AlreadyExists: Das Kontingent existiert bereits für diese Einheit NotFound: Kontingent für diese Einheit nicht gefunden diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 603cbac27..36c9de9eb 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -60,7 +60,6 @@ Errors: Notification: NoDomain: No Domain found for message User: - TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: User could not be found AlreadyExists: User already exists NotFoundOnOrg: User could not be found on chosen organization @@ -479,6 +478,7 @@ Errors: CloseRows: SQL Statement could not be finished SQLStatement: SQL Statement could not be created InvalidRequest: Request is invalid + TooManyNestingLevels: Too many query nesting levels (Max 20) Quota: AlreadyExists: Quota already exists for this unit NotFound: Quota not found for this unit diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 3eea6faf6..15d29456f 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -60,7 +60,6 @@ Errors: Notification: NoDomain: No se encontró el dominio para el mensaje User: - TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: El usuario no pudo encontrarse AlreadyExists: El usuario ya existe NotFoundOnOrg: El usuario no pudo encontrarse en la organización elegida @@ -479,6 +478,7 @@ Errors: CloseRows: La sentencia SQL no pudo finalizarse SQLStatement: La sentencia SQL no pudo crearse InvalidRequest: La solicitud no es válida + TooManyNestingLevels: Demasiados niveles de anidamiento de consultas (máximo 20) Quota: AlreadyExists: La cuota ya existe para esta unidad NotFound: Cuota no encontrada para esta unidad diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 90b14892a..5f029e9b8 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -60,7 +60,6 @@ Errors: Notification: NoDomain: Aucun domaine trouvé pour le message User: - TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: L'utilisateur n'a pas été trouvé AlreadyExists: L'utilisateur existe déjà NotFoundOnOrg: L'utilisateur n'a pas été trouvé dans l'organisation choisie @@ -479,6 +478,7 @@ Errors: CloseRows: L'instruction SQL n'a pas pu être terminée SQLStatement: L'instruction SQL n'a pas pu être créée InvalidRequest: La requête n'est pas valide + TooManyNestingLevels: Trop de niveaux d'imbrication de requêtes (maximum 20) Quota: AlreadyExists: Contingent existe déjà pour cette unité NotFound: Contingent non trouvé pour cette unité diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 08e38a2d8..e7d0ffabe 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -60,7 +60,6 @@ Errors: Notification: NoDomain: Nessun dominio trovato per il messaggio User: - TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: L'utente non è stato trovato AlreadyExists: L'utente già esistente NotFoundOnOrg: L'utente non è stato trovato nell'organizzazione scelta @@ -480,6 +479,7 @@ Errors: CloseRows: Lo statement SQL non può essere terminato SQLStatement: Lo statement SQL non può essere creato InvalidRequest: La richiesta non è valida + TooManyNestingLevels: Troppi livelli di nidificazione delle query (massimo 20) Quota: AlreadyExists: La quota esiste già per questa unità NotFound: Quota non trovata per questa unità diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 26fbf5373..845104173 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -60,7 +60,6 @@ Errors: Notification: NoDomain: メッセージのドメインが見つかりません User: - TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: ユーザーが見つかりません AlreadyExists: 既に存在するユーザーです NotFoundOnOrg: ユーザーが選択した組織内で見つかりません @@ -468,6 +467,7 @@ Errors: CloseRows: SQLステートメントの終了に失敗しました SQLStatement: SQLステートメントの作成に失敗しました InvalidRequest: 無効なリクエストです + TooManyNestingLevels: クエリのネスト レベルが多すぎます (最大 20) Quota: AlreadyExists: このユニットにはすでにクォータが存在しています NotFound: このユニットにはクォータが見つかりません diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 2737da7f2..3b6b5a2e3 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -60,7 +60,6 @@ Errors: Notification: NoDomain: Не е пронајден домен за пораката User: - TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: Корисникот не е пронајден AlreadyExists: Корисникот веќе постои NotFoundOnOrg: Корисникот не е пронајден во избраната организација @@ -478,6 +477,7 @@ Errors: CloseRows: SQL наредбата не може да се заврши SQLStatement: SQL наредбата не може да се креира InvalidRequest: Барањето е невалидно + TooManyNestingLevels: Премногу нивоа на вгнездување на барања (макс 20) Quota: AlreadyExists: Веќе постои квота за оваа единица NotFound: Квотата не е пронајдена за оваа единица diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 5337ff3f4..0c64a13c0 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -60,7 +60,6 @@ Errors: Notification: NoDomain: Geen domein gevonden voor bericht User: - TooManyNestingLevels: Te veel query nesting niveaus (Max 20). NotFound: Gebruiker kon niet worden gevonden AlreadyExists: Gebruiker bestaat al NotFoundOnOrg: Gebruiker kon niet worden gevonden op gekozen organisatie @@ -479,6 +478,7 @@ Errors: CloseRows: SQL Statement kon niet worden voltooid SQLStatement: SQL Statement kon niet worden gemaakt InvalidRequest: Verzoek is ongeldig + TooManyNestingLevels: Te veel query nesting niveaus (Max 20) Quota: AlreadyExists: Quota bestaat al voor deze eenheid NotFound: Quota niet gevonden voor deze eenheid diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 3b1771218..38381a2c3 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -60,7 +60,6 @@ Errors: Notification: NoDomain: Nie znaleziono domeny dla wiadomości User: - TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: Nie znaleziono użytkownika AlreadyExists: Użytkownik już istnieje NotFoundOnOrg: Użytkownik nie został znaleziony w wybranej organizacji @@ -479,6 +478,7 @@ Errors: CloseRows: Instrukcja SQL nie mogła zostać zakończona SQLStatement: Instrukcja SQL nie mogła zostać utworzona InvalidRequest: Żądanie jest nieprawidłowe + TooManyNestingLevels: Zbyt wiele poziomów zagnieżdżenia zapytań (maks. 20) Quota: AlreadyExists: Limit już istnieje dla tej jednostki NotFound: Nie znaleziono limitu dla tej jednostki diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 8c6442aea..b7faeb7f1 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -60,7 +60,6 @@ Errors: Notification: NoDomain: Nenhum domínio encontrado para a mensagem User: - TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: Usuário não pôde ser encontrado AlreadyExists: Usuário já existe NotFoundOnOrg: Usuário não pôde ser encontrado na organização escolhida @@ -477,6 +476,7 @@ Errors: CloseRows: A instrução SQL não pôde ser concluída SQLStatement: Não foi possível criar a instrução SQL InvalidRequest: O pedido é inválido + TooManyNestingLevels: muitos níveis de aninhamento de consulta (máx. 20) Quota: AlreadyExists: Cota já existe para esta unidade NotFound: Cota não encontrada para esta unidade diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 6bf62864d..0c0847d36 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -471,6 +471,7 @@ Errors: CloseRows: SQL-запрос не удалось завершить SQLStatement: SQL-запрос не может быть создан InvalidRequest: Запрос недействителен + TooManyNestingLevels: слишком много уровней вложенности запросов (максимум 20) Quota: AlreadyExists: Квота для данного объекта уже существует NotFound: Квота для данного объекта не найдена diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index a6fbb3eba..2a92127a7 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -60,7 +60,6 @@ Errors: Notification: NoDomain: 未找到对应的域名 User: - TooManyNestingLevels: Too many query nesting levels (Max 20). NotFound: 找不到用户 AlreadyExists: 用户已存在 NotFoundOnOrg: 在所选组织中找不到用户 @@ -479,6 +478,7 @@ Errors: CloseRows: SQL 语句无法完成 SQLStatement: 无法创建 SQL 语句 InvalidRequest: 请求无效 + TooManyNestingLevels: 查询嵌套级别过多(最多 20 个) Quota: AlreadyExists: 这个单位的配额已经存在 NotFound: 没有找到该单位的配额 diff --git a/proto/zitadel/user/schema/v3alpha/user_schema.proto b/proto/zitadel/user/schema/v3alpha/user_schema.proto index 1b5be71a7..c75d7d819 100644 --- a/proto/zitadel/user/schema/v3alpha/user_schema.proto +++ b/proto/zitadel/user/schema/v3alpha/user_schema.proto @@ -32,7 +32,7 @@ message UserSchema { example: "\"STATE_ACTIVE\"" } ]; - // Revision is a read only version of the schema, each update increases the revision. + // Revision is a read only version of the schema, each update of the `schema`-field increases the revision. uint32 revision = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"2\"" @@ -59,7 +59,7 @@ enum FieldName { FIELD_NAME_TYPE = 1; FIELD_NAME_STATE = 2; FIELD_NAME_REVISION = 3; - FIELD_NAME_CREATION_DATE = 4; + FIELD_NAME_CHANGE_DATE = 4; } message SearchQuery { @@ -79,6 +79,8 @@ message SearchQuery { TypeQuery type_query = 5; // Limit the result to a specific state of the schema. StateQuery state_query = 6; + // Limit the result to a specific schema ID. + IDQuery id_query = 7; } }