mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-06 13:57:41 +00:00
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:
parent
5b301c7f96
commit
7494a7b6d9
@ -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"
|
||||
|
@ -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.
|
@ -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:
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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())
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
203
internal/query/projection/user_schema.go
Normal file
203
internal/query/projection/user_schema.go
Normal 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
|
||||
}
|
219
internal/query/projection/user_schema_test.go
Normal file
219
internal/query/projection/user_schema_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
218
internal/query/user_schema.go
Normal file
218
internal/query/user_schema.go
Normal 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
|
||||
}
|
||||
}
|
290
internal/query/user_schema_test.go
Normal file
290
internal/query/user_schema_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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: Не е намерена квота за тази единица
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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é
|
||||
|
@ -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à
|
||||
|
@ -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: このユニットにはクォータが見つかりません
|
||||
|
@ -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: Квотата не е пронајдена за оваа единица
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -471,6 +471,7 @@ Errors:
|
||||
CloseRows: SQL-запрос не удалось завершить
|
||||
SQLStatement: SQL-запрос не может быть создан
|
||||
InvalidRequest: Запрос недействителен
|
||||
TooManyNestingLevels: слишком много уровней вложенности запросов (максимум 20)
|
||||
Quota:
|
||||
AlreadyExists: Квота для данного объекта уже существует
|
||||
NotFound: Квота для данного объекта не найдена
|
||||
|
@ -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: 没有找到该单位的配额
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user