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.
This commit is contained in:
Livio Spring 2024-03-22 14:26:13 +01:00 committed by GitHub
parent 5b301c7f96
commit 7494a7b6d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1597 additions and 19 deletions

View File

@ -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"

View File

@ -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.

View File

@ -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:

View File

@ -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)
}

View File

@ -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")
})
}
}

View File

@ -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:

View File

@ -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

View File

@ -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,
},

View File

@ -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())

View File

@ -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,
}
}

View File

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

View File

@ -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)
})
}
}

View File

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

View File

@ -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)
})
}
}

View File

@ -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: Не е намерена квота за тази единица

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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é

View File

@ -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à

View File

@ -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: このユニットにはクォータが見つかりません

View File

@ -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: Квотата не е пронајдена за оваа единица

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -471,6 +471,7 @@ Errors:
CloseRows: SQL-запрос не удалось завершить
SQLStatement: SQL-запрос не может быть создан
InvalidRequest: Запрос недействителен
TooManyNestingLevels: слишком много уровней вложенности запросов (максимум 20)
Quota:
AlreadyExists: Квота для данного объекта уже существует
NotFound: Квота для данного объекта не найдена

View File

@ -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: 没有找到该单位的配额

View File

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