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; } }