feat: implement user schema management (#7416)

This PR adds the functionality to manage user schemas through the new user schema service.
It includes the possibility to create a basic JSON schema and also provides a way on defining permissions (read, write) for owner and self context with an annotation.

Further annotations for OIDC claims and SAML attribute mappings will follow.

A guide on how to create a schema and assign permissions has been started. It will be extended though out the process of implementing the schema and users based on those.

Note:
This feature is in an early stage and therefore not enabled by default. To test it out, please enable the UserSchema feature flag on your instance / system though the feature service.
This commit is contained in:
Livio Spring 2024-03-12 14:50:13 +01:00 committed by GitHub
parent 2a39cc16f5
commit 0e181b218c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 3614 additions and 35 deletions

View File

@ -832,7 +832,7 @@ DefaultInstance:
Greeting: Hello {{.DisplayName}},
Text: The password of your user has changed. If this change was not done by you, please be advised to immediately reset your password.
ButtonText: Login
# Once a feature is set on the instance (true or false), system level feature settings
# will be ignored until instance level features are reset.
Features:
@ -1016,6 +1016,8 @@ InternalAuthZ:
- "execution.read"
- "execution.write"
- "execution.delete"
- "userschema.write"
- "userschema.delete"
- Role: "IAM_OWNER_VIEWER"
Permissions:
- "iam.read"

View File

@ -75,6 +75,7 @@ DefaultInstance:
LoginDefaultOrg: true
LegacyIntrospection: true
TriggerIntrospectionProjections: true
UserSchema: true
Log:
Level: info
Actions:
@ -86,6 +87,7 @@ Actions:
LoginDefaultOrg: gu.Ptr(true),
LegacyIntrospection: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(true),
UserSchema: gu.Ptr(true),
})
},
}, {

View File

@ -44,6 +44,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/session/v2"
"github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
"github.com/zitadel/zitadel/internal/api/grpc/system"
user_schema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/user/schema/v3alpha"
user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/http/middleware"
@ -410,6 +411,9 @@ func startAPIs(
if err := apis.RegisterService(ctx, execution_v3_alpha.CreateServer(commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, user_schema_v3_alpha.CreateServer(commands, queries)); err != nil {
return nil, err
}
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, config.ExternalDomain, login.IgnoreInstanceEndpoints...)
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))

View File

@ -0,0 +1,135 @@
---
title: User Schema
---
ZITADEL allows you to define schemas for users, based on the [JSON Schema Standard](https://json-schema.org/).
This gives you the possibility to define your own data models for your users, validate them based on these definitions
and making sure who has access or manipulate information of the user.
By defining multiple schemas, you can even differentiate between different personas of your organization or application
and restrictions, resp. requirements for them to authenticate.
For example, you could have separate schemas for your employees and your customers. While you might want to make sure that
certain data like given name and family name are required for employees, they might be optional for the latter.
Or you might want to disable username password authentication for your admins and only allow phishing resistant methods like passkeys,
but still allow it for your customers.
:::info
Please be aware that User Schema is in a [preview stage](/support/software-release-cycles-support#preview) not feature complete
and therefore not generally available.
Do not use it for production yet. To test it out, you need to enable the `UserSchema` [feature flag](/apis/resources/feature_service_v2/feature-service).
:::
## Create your first schema
Let's create the first very simple schema `user`, which defines a `givenName` and `familyName` for a user and allows them to
authenticate with username and password.
We can do so by calling the [create user schema endpoint](/docs/apis/resources/user_schema_service_v3/user-schema-service-create-user-schema)
with the following data. Make sure to provide an access_token with an IAM_OWNER role.
```bash
curl -X POST "https://$CUSTOM-DOMAIN/v3alpha/user_schemas" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
--data-raw '{
"type": "user",
"schema": {
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"givenName": {
"type": "string"
},
"familyName": {
"type": "string"
}
}
},
"possibleAuthenticators": [
"AUTHENTICATOR_TYPE_USERNAME",
"AUTHENTICATOR_TYPE_PASSWORD"
]
}'
```
This will return something similar to:
```json
{
"id": "257199613398745508",
"details": {
"sequence": "2",
"change_date": "2024-03-07T08:08:35.963956Z",
"resource_owner": "253750309325636004"
}
}
```
So you successfully create a schema and could use that to manage your users based on that.
But let's first checkout some possibilities ZITADEL offers.
## Assign Permissions
In the first step we've created a very simple `user` schema with only `givenName` and `familyName`.
This allows any user with the permission to edit the user's data to change these values.
Let's now update the schema and add some more properties and restrict who's able to retrieve and change data.
By setting `urn:zitadel:schema:permission` to fields, we can define the permissions for that field of different user roles / context.
For example by adding it to the `givenName` and `familyName` we can keep the state from before, where any `owner` (e.g. ORG_OWNER)
as well as the user themselves (`self`) are allowed to read (`r`) and write (`w`) the data.
Let's now assume our service provides some profile information of the user on a dedicated page.
Since we do not want the user to be able to change that value, we set the permission of `self` to `r`, meaning they will be able
to see the `profileUri` value, but cannot update it.
Maybe we also have some `customerId`, which the user should not even know about. We therefore can simply omit the `self` permission
and only set `owner` to `rw`, so admins are able to read and change the id if needed.
Finally, we call the [update user schema endpoint](/docs/apis/resources/user_schema_service_v3/user-schema-service-update-user-schema)
with the following data. Be sure to provide the id of the previously created schema.
```bash
curl -X PUT "https://$CUSTOM-DOMAIN/v3alpha/user_schemas/$SCHEMA_ID" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
--data-raw '{
"schema": {
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"givenName": {
"type": "string",
"urn:zitadel:schema:permission": {
"owner": "rw",
"self": "rw"
}
},
"familyName": {
"type": "string",
"urn:zitadel:schema:permission": {
"owner": "rw",
"self": "rw"
}
},
"profileUri": {
"type": "string",
"format": "uri",
"urn:zitadel:schema:permission": {
"owner": "rw",
"self": "r"
}
},
"customerId": {
"type": "string",
"urn:zitadel:schema:permission": {
"owner": "rw"
}
}
}
}
}
```

View File

@ -165,6 +165,7 @@ module.exports = {
items: [
"guides/manage/user/reg-create-user",
"guides/manage/customize/user-metadata",
"guides/manage/customize/user-schema",
],
},
],

1
go.mod
View File

@ -54,6 +54,7 @@ require (
github.com/pquerna/otp v1.4.0
github.com/rakyll/statik v0.1.7
github.com/rs/cors v1.10.1
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/sony/sonyflake v1.2.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2

2
go.sum
View File

@ -694,6 +694,8 @@ github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgY
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=

View File

@ -13,6 +13,7 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.
LoginDefaultOrg: req.LoginDefaultOrg,
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
LegacyIntrospection: req.OidcLegacyIntrospection,
UserSchema: req.UserSchema,
}
}
@ -22,6 +23,7 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
UserSchema: featureSourceToFlagPb(&f.UserSchema),
}
}
@ -30,6 +32,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm
LoginDefaultOrg: req.LoginDefaultOrg,
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
LegacyIntrospection: req.OidcLegacyIntrospection,
UserSchema: req.UserSchema,
}
}
@ -39,6 +42,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
UserSchema: featureSourceToFlagPb(&f.UserSchema),
}
}

View File

@ -21,11 +21,13 @@ func Test_systemFeaturesToCommand(t *testing.T) {
LoginDefaultOrg: gu.Ptr(true),
OidcTriggerIntrospectionProjections: gu.Ptr(false),
OidcLegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
}
want := &command.SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
}
got := systemFeaturesToCommand(arg)
assert.Equal(t, want, got)
@ -50,6 +52,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
Level: feature.LevelSystem,
Value: true,
},
UserSchema: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
}
want := &feature_pb.GetSystemFeaturesResponse{
Details: &object.Details{
@ -69,6 +75,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
UserSchema: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
}
got := systemFeaturesToPb(arg)
assert.Equal(t, want, got)
@ -79,11 +89,13 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
LoginDefaultOrg: gu.Ptr(true),
OidcTriggerIntrospectionProjections: gu.Ptr(false),
OidcLegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
}
want := &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
}
got := instanceFeaturesToCommand(arg)
assert.Equal(t, want, got)
@ -108,6 +120,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Level: feature.LevelInstance,
Value: true,
},
UserSchema: query.FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
}
want := &feature_pb.GetInstanceFeaturesResponse{
Details: &object.Details{
@ -127,6 +143,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
UserSchema: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
}
got := instanceFeaturesToPb(arg)
assert.Equal(t, want, got)

View File

@ -218,6 +218,7 @@ func TestServer_GetSystemFeatures(t *testing.T) {
assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema)
})
}
}
@ -384,6 +385,10 @@ func TestServer_GetInstanceFeatures(t *testing.T) {
Enabled: true,
Source: feature.Source_SOURCE_SYSTEM,
},
UserSchema: &feature.FeatureFlag{
Enabled: false,
Source: feature.Source_SOURCE_UNSPECIFIED,
},
},
},
{
@ -392,6 +397,7 @@ func TestServer_GetInstanceFeatures(t *testing.T) {
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
LoginDefaultOrg: gu.Ptr(true),
OidcTriggerIntrospectionProjections: gu.Ptr(false),
UserSchema: gu.Ptr(true),
})
require.NoError(t, err)
},
@ -408,6 +414,10 @@ func TestServer_GetInstanceFeatures(t *testing.T) {
Enabled: false,
Source: feature.Source_SOURCE_INSTANCE,
},
UserSchema: &feature.FeatureFlag{
Enabled: true,
Source: feature.Source_SOURCE_INSTANCE,
},
},
},
{
@ -437,6 +447,10 @@ func TestServer_GetInstanceFeatures(t *testing.T) {
Enabled: true,
Source: feature.Source_SOURCE_SYSTEM,
},
UserSchema: &feature.FeatureFlag{
Enabled: false,
Source: feature.Source_SOURCE_UNSPECIFIED,
},
},
},
}
@ -459,6 +473,7 @@ func TestServer_GetInstanceFeatures(t *testing.T) {
assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema)
})
}
}

View File

@ -0,0 +1,150 @@
package schema
import (
"context"
"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/zerrors"
schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"
)
func (s *Server) CreateUserSchema(ctx context.Context, req *schema.CreateUserSchemaRequest) (*schema.CreateUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
userSchema, err := createUserSchemaToCommand(req, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
id, details, err := s.command.CreateUserSchema(ctx, userSchema)
if err != nil {
return nil, err
}
return &schema.CreateUserSchemaResponse{
Id: id,
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) UpdateUserSchema(ctx context.Context, req *schema.UpdateUserSchemaRequest) (*schema.UpdateUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
userSchema, err := updateUserSchemaToCommand(req, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
details, err := s.command.UpdateUserSchema(ctx, userSchema)
if err != nil {
return nil, err
}
return &schema.UpdateUserSchemaResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) DeactivateUserSchema(ctx context.Context, req *schema.DeactivateUserSchemaRequest) (*schema.DeactivateUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.DeactivateUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &schema.DeactivateUserSchemaResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) ReactivateUserSchema(ctx context.Context, req *schema.ReactivateUserSchemaRequest) (*schema.ReactivateUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.ReactivateUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &schema.ReactivateUserSchemaResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) DeleteUserSchema(ctx context.Context, req *schema.DeleteUserSchemaRequest) (*schema.DeleteUserSchemaResponse, error) {
if err := checkUserSchemaEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.DeleteUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &schema.DeleteUserSchemaResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func checkUserSchemaEnabled(ctx context.Context) error {
if authz.GetInstance(ctx).Features().UserSchema {
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "SCHEMA-SFjk3", "Errors.UserSchema.NotEnabled")
}
func createUserSchemaToCommand(req *schema.CreateUserSchemaRequest, resourceOwner string) (*command.CreateUserSchema, error) {
schema, err := req.GetSchema().MarshalJSON()
if err != nil {
return nil, err
}
return &command.CreateUserSchema{
ResourceOwner: resourceOwner,
Type: req.GetType(),
Schema: schema,
PossibleAuthenticators: authenticatorsToDomain(req.GetPossibleAuthenticators()),
}, nil
}
func updateUserSchemaToCommand(req *schema.UpdateUserSchemaRequest, resourceOwner string) (*command.UpdateUserSchema, error) {
schema, err := req.GetSchema().MarshalJSON()
if err != nil {
return nil, err
}
return &command.UpdateUserSchema{
ID: req.GetId(),
ResourceOwner: resourceOwner,
Type: req.Type,
Schema: schema,
PossibleAuthenticators: authenticatorsToDomain(req.GetPossibleAuthenticators()),
}, nil
}
func authenticatorsToDomain(authenticators []schema.AuthenticatorType) []domain.AuthenticatorType {
types := make([]domain.AuthenticatorType, len(authenticators))
for i, authenticator := range authenticators {
types[i] = authenticatorTypeToDomain(authenticator)
}
return types
}
func authenticatorTypeToDomain(authenticator schema.AuthenticatorType) domain.AuthenticatorType {
switch authenticator {
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED:
return domain.AuthenticatorTypeUnspecified
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME:
return domain.AuthenticatorTypeUsername
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD:
return domain.AuthenticatorTypePassword
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN:
return domain.AuthenticatorTypeWebAuthN
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP:
return domain.AuthenticatorTypeTOTP
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL:
return domain.AuthenticatorTypeOTPEmail
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS:
return domain.AuthenticatorTypeOTPSMS
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY:
return domain.AuthenticatorTypeAuthenticationKey
case schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER:
return domain.AuthenticatorTypeIdentityProvider
default:
return domain.AuthenticatorTypeUnspecified
}
}

View File

@ -0,0 +1,812 @@
//go:build integration
package schema_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"
)
var (
CTX context.Context
Tester *integration.Tester
Client schema.UserSchemaServiceClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
CTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
Client = Tester.Client.UserSchemaV3
return m.Run()
}())
}
func ensureFeatureEnabled(t *testing.T) {
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(t, err)
if f.UserSchema.GetEnabled() {
return
}
_, err = Tester.Client.FeatureV2.SetInstanceFeatures(CTX, &feature.SetInstanceFeaturesRequest{
UserSchema: gu.Ptr(true),
})
require.NoError(t, err)
retryDuration := time.Minute
if ctxDeadline, ok := CTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(ttt, err)
if f.UserSchema.GetEnabled() {
return
}
},
retryDuration,
100*time.Millisecond,
"timed out waiting for ensuring instance feature")
}
func TestServer_CreateUserSchema(t *testing.T) {
tests := []struct {
name string
ctx context.Context
req *schema.CreateUserSchemaRequest
want *schema.CreateUserSchemaResponse
wantErr bool
}{
{
name: "missing permission, error",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &schema.CreateUserSchemaRequest{
Type: fmt.Sprint(time.Now().UnixNano() + 1),
},
wantErr: true,
},
{
name: "empty type",
ctx: CTX,
req: &schema.CreateUserSchemaRequest{
Type: "",
},
wantErr: true,
},
{
name: "empty schema, error",
ctx: CTX,
req: &schema.CreateUserSchemaRequest{
Type: fmt.Sprint(time.Now().UnixNano() + 1),
},
wantErr: true,
},
{
name: "invalid schema, error",
ctx: CTX,
req: &schema.CreateUserSchemaRequest{
Type: fmt.Sprint(time.Now().UnixNano() + 1),
DataType: &schema.CreateUserSchemaRequest_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true
},
"description": {
"type": "string"
}
}
}
`))
require.NoError(t, err)
return s
}(),
},
},
wantErr: true,
},
{
name: "no authenticators, ok",
ctx: CTX,
req: &schema.CreateUserSchemaRequest{
Type: fmt.Sprint(time.Now().UnixNano() + 1),
DataType: &schema.CreateUserSchemaRequest_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
},
want: &schema.CreateUserSchemaResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "invalid authenticator, error",
ctx: CTX,
req: &schema.CreateUserSchemaRequest{
Type: fmt.Sprint(time.Now().UnixNano() + 1),
DataType: &schema.CreateUserSchemaRequest_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED,
},
},
wantErr: true,
},
{
name: "with authenticator, ok",
ctx: CTX,
req: &schema.CreateUserSchemaRequest{
Type: fmt.Sprint(time.Now().UnixNano() + 1),
DataType: &schema.CreateUserSchemaRequest_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME,
},
},
want: &schema.CreateUserSchemaResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "with invalid permission, error",
ctx: CTX,
req: &schema.CreateUserSchemaRequest{
Type: fmt.Sprint(time.Now().UnixNano() + 1),
DataType: &schema.CreateUserSchemaRequest_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string",
"urn:zitadel:schema:permission": "read"
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME,
},
},
wantErr: true,
},
{
name: "with valid permission, ok",
ctx: CTX,
req: &schema.CreateUserSchemaRequest{
Type: fmt.Sprint(time.Now().UnixNano() + 1),
DataType: &schema.CreateUserSchemaRequest_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string",
"urn:zitadel:schema:permission": {
"owner": "rw",
"self": "r"
}
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME,
},
},
want: &schema.CreateUserSchemaResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ensureFeatureEnabled(t)
got, err := Client.CreateUserSchema(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
assert.NotEmpty(t, got.GetId())
})
}
}
func TestServer_UpdateUserSchema(t *testing.T) {
type args struct {
ctx context.Context
req *schema.UpdateUserSchemaRequest
}
tests := []struct {
name string
prepare func(request *schema.UpdateUserSchemaRequest) error
args args
want *schema.UpdateUserSchemaResponse
wantErr bool
}{
{
name: "missing permission, error",
prepare: func(request *schema.UpdateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &schema.UpdateUserSchemaRequest{
Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
wantErr: true,
},
{
name: "missing id, error",
prepare: func(request *schema.UpdateUserSchemaRequest) error {
return nil
},
args: args{
ctx: CTX,
req: &schema.UpdateUserSchemaRequest{},
},
wantErr: true,
},
{
name: "not existing, error",
prepare: func(request *schema.UpdateUserSchemaRequest) error {
request.Id = "notexisting"
return nil
},
args: args{
ctx: CTX,
req: &schema.UpdateUserSchemaRequest{},
},
wantErr: true,
},
{
name: "empty type, error",
prepare: func(request *schema.UpdateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: CTX,
req: &schema.UpdateUserSchemaRequest{
Type: gu.Ptr(""),
},
},
wantErr: true,
},
{
name: "update type, ok",
prepare: func(request *schema.UpdateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: CTX,
req: &schema.UpdateUserSchemaRequest{
Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
want: &schema.UpdateUserSchemaResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "empty schema, ok",
prepare: func(request *schema.UpdateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: CTX,
req: &schema.UpdateUserSchemaRequest{
DataType: &schema.UpdateUserSchemaRequest_Schema{},
},
},
want: &schema.UpdateUserSchemaResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "invalid schema, error",
prepare: func(request *schema.UpdateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: CTX,
req: &schema.UpdateUserSchemaRequest{
DataType: &schema.UpdateUserSchemaRequest_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true
},
"description": {
"type": "string"
}
}
}
`))
require.NoError(t, err)
return s
}(),
},
},
},
wantErr: true,
},
{
name: "update schema, ok",
prepare: func(request *schema.UpdateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: CTX,
req: &schema.UpdateUserSchemaRequest{
DataType: &schema.UpdateUserSchemaRequest_Schema{
Schema: func() *structpb.Struct {
s := new(structpb.Struct)
err := s.UnmarshalJSON([]byte(`
{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["name"]
}
`))
require.NoError(t, err)
return s
}(),
},
},
},
want: &schema.UpdateUserSchemaResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "invalid authenticator, error",
prepare: func(request *schema.UpdateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: CTX,
req: &schema.UpdateUserSchemaRequest{
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED,
},
},
},
wantErr: true,
},
{
name: "update authenticator, ok",
prepare: func(request *schema.UpdateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
return nil
},
args: args{
ctx: CTX,
req: &schema.UpdateUserSchemaRequest{
PossibleAuthenticators: []schema.AuthenticatorType{
schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME,
},
},
},
want: &schema.UpdateUserSchemaResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "inactive, error",
prepare: func(request *schema.UpdateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
_, err := Client.DeactivateUserSchema(CTX, &schema.DeactivateUserSchemaRequest{
Id: schemaID,
})
require.NoError(t, err)
request.Id = schemaID
return nil
},
args: args{
ctx: CTX,
req: &schema.UpdateUserSchemaRequest{
Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ensureFeatureEnabled(t)
err := tt.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.UpdateUserSchema(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_DeactivateUserSchema(t *testing.T) {
type args struct {
ctx context.Context
req *schema.DeactivateUserSchemaRequest
prepare func(request *schema.DeactivateUserSchemaRequest) error
}
tests := []struct {
name string
args args
want *schema.DeactivateUserSchemaResponse
wantErr bool
}{
{
name: "not existing, error",
args: args{
CTX,
&schema.DeactivateUserSchemaRequest{
Id: "notexisting",
},
func(request *schema.DeactivateUserSchemaRequest) error { return nil },
},
wantErr: true,
},
{
name: "active, ok",
args: args{
CTX,
&schema.DeactivateUserSchemaRequest{},
func(request *schema.DeactivateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
return nil
},
},
want: &schema.DeactivateUserSchemaResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "inactive, error",
args: args{
CTX,
&schema.DeactivateUserSchemaRequest{},
func(request *schema.DeactivateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
_, err := Client.DeactivateUserSchema(CTX, &schema.DeactivateUserSchemaRequest{
Id: schemaID,
})
return err
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ensureFeatureEnabled(t)
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.DeactivateUserSchema(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_ReactivateUserSchema(t *testing.T) {
type args struct {
ctx context.Context
req *schema.ReactivateUserSchemaRequest
prepare func(request *schema.ReactivateUserSchemaRequest) error
}
tests := []struct {
name string
args args
want *schema.ReactivateUserSchemaResponse
wantErr bool
}{
{
name: "not existing, error",
args: args{
CTX,
&schema.ReactivateUserSchemaRequest{
Id: "notexisting",
},
func(request *schema.ReactivateUserSchemaRequest) error { return nil },
},
wantErr: true,
},
{
name: "active, error",
args: args{
ctx: CTX,
req: &schema.ReactivateUserSchemaRequest{},
prepare: func(request *schema.ReactivateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
return nil
},
},
wantErr: true,
},
{
name: "inactive, ok",
args: args{
ctx: CTX,
req: &schema.ReactivateUserSchemaRequest{},
prepare: func(request *schema.ReactivateUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
_, err := Client.DeactivateUserSchema(CTX, &schema.DeactivateUserSchemaRequest{
Id: schemaID,
})
return err
},
},
want: &schema.ReactivateUserSchemaResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ensureFeatureEnabled(t)
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.ReactivateUserSchema(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_DeleteUserSchema(t *testing.T) {
type args struct {
ctx context.Context
req *schema.DeleteUserSchemaRequest
prepare func(request *schema.DeleteUserSchemaRequest) error
}
tests := []struct {
name string
args args
want *schema.DeleteUserSchemaResponse
wantErr bool
}{
{
name: "not existing, error",
args: args{
CTX,
&schema.DeleteUserSchemaRequest{
Id: "notexisting",
},
func(request *schema.DeleteUserSchemaRequest) error { return nil },
},
wantErr: true,
},
{
name: "delete, ok",
args: args{
ctx: CTX,
req: &schema.DeleteUserSchemaRequest{},
prepare: func(request *schema.DeleteUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
return nil
},
},
want: &schema.DeleteUserSchemaResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "deleted, error",
args: args{
ctx: CTX,
req: &schema.DeleteUserSchemaRequest{},
prepare: func(request *schema.DeleteUserSchemaRequest) error {
schemaID := Tester.CreateUserSchema(CTX, t).GetId()
request.Id = schemaID
_, err := Client.DeleteUserSchema(CTX, &schema.DeleteUserSchemaRequest{
Id: schemaID,
})
return err
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ensureFeatureEnabled(t)
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.DeleteUserSchema(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,51 @@
package schema
import (
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"
)
var _ schema.UserSchemaServiceServer = (*Server)(nil)
type Server struct {
schema.UnimplementedUserSchemaServiceServer
command *command.Commands
query *query.Queries
}
type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
) *Server {
return &Server{
command: command,
query: query,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
schema.RegisterUserSchemaServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return schema.UserSchemaService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return schema.UserSchemaService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return schema.UserSchemaService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return schema.RegisterUserSchemaServiceHandler
}

View File

@ -15,12 +15,14 @@ type InstanceFeatures struct {
LoginDefaultOrg *bool
TriggerIntrospectionProjections *bool
LegacyIntrospection *bool
UserSchema *bool
}
func (m *InstanceFeatures) isEmpty() bool {
return m.LoginDefaultOrg == nil &&
m.TriggerIntrospectionProjections == nil &&
m.LegacyIntrospection == nil
m.LegacyIntrospection == nil &&
m.UserSchema == nil
}
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {

View File

@ -54,6 +54,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceLoginDefaultOrgEventType,
feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
feature_v2.InstanceLegacyIntrospectionEventType,
feature_v2.InstanceUserSchemaEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@ -62,6 +63,7 @@ func (m *InstanceFeaturesWriteModel) reduceReset() {
m.LoginDefaultOrg = nil
m.TriggerIntrospectionProjections = nil
m.LegacyIntrospection = nil
m.UserSchema = nil
}
func (m *InstanceFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
@ -78,6 +80,8 @@ func (m *InstanceFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEven
m.TriggerIntrospectionProjections = &event.Value
case feature.KeyLegacyIntrospection:
m.LegacyIntrospection = &event.Value
case feature.KeyUserSchema:
m.UserSchema = &event.Value
}
return nil
}
@ -88,5 +92,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginDefaultOrg, f.LoginDefaultOrg, feature_v2.InstanceLoginDefaultOrgEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TriggerIntrospectionProjections, f.TriggerIntrospectionProjections, feature_v2.InstanceTriggerIntrospectionProjectionsEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.InstanceLegacyIntrospectionEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType)
return cmds
}

View File

@ -131,6 +131,24 @@ func TestCommands_SetInstanceFeatures(t *testing.T) {
ResourceOwner: "instance1",
},
},
{
name: "set UserSchema",
eventstore: expectEventstore(
expectFilter(),
expectPush(
feature_v2.NewSetEvent[bool](
ctx, aggregate,
feature_v2.InstanceUserSchemaEventType, true,
),
),
),
args: args{ctx, &InstanceFeatures{
UserSchema: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "instance1",
},
},
{
name: "push error",
eventstore: expectEventstore(
@ -164,12 +182,17 @@ func TestCommands_SetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceLegacyIntrospectionEventType, true,
),
feature_v2.NewSetEvent[bool](
ctx, aggregate,
feature_v2.InstanceUserSchemaEventType, true,
),
),
),
args: args{ctx, &InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: gu.Ptr(true),
UserSchema: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "instance1",

View File

@ -12,12 +12,14 @@ type SystemFeatures struct {
LoginDefaultOrg *bool
TriggerIntrospectionProjections *bool
LegacyIntrospection *bool
UserSchema *bool
}
func (m *SystemFeatures) isEmpty() bool {
return m.LoginDefaultOrg == nil &&
m.TriggerIntrospectionProjections == nil &&
m.LegacyIntrospection == nil
m.LegacyIntrospection == nil &&
m.UserSchema == nil
}
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {

View File

@ -49,6 +49,7 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemLoginDefaultOrgEventType,
feature_v2.SystemTriggerIntrospectionProjectionsEventType,
feature_v2.SystemLegacyIntrospectionEventType,
feature_v2.SystemUserSchemaEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@ -57,6 +58,7 @@ func (m *SystemFeaturesWriteModel) reduceReset() {
m.LoginDefaultOrg = nil
m.TriggerIntrospectionProjections = nil
m.LegacyIntrospection = nil
m.UserSchema = nil
}
func (m *SystemFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
@ -73,6 +75,8 @@ func (m *SystemFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[
m.TriggerIntrospectionProjections = &event.Value
case feature.KeyLegacyIntrospection:
m.LegacyIntrospection = &event.Value
case feature.KeyUserSchema:
m.UserSchema = &event.Value
}
return nil
}
@ -83,6 +87,7 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginDefaultOrg, f.LoginDefaultOrg, feature_v2.SystemLoginDefaultOrgEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TriggerIntrospectionProjections, f.TriggerIntrospectionProjections, feature_v2.SystemTriggerIntrospectionProjectionsEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.SystemLegacyIntrospectionEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.SystemUserSchemaEventType)
return cmds
}

View File

@ -99,6 +99,24 @@ func TestCommands_SetSystemFeatures(t *testing.T) {
ResourceOwner: "SYSTEM",
},
},
{
name: "set UserSchema",
eventstore: expectEventstore(
expectFilter(),
expectPush(
feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, true,
),
),
),
args: args{context.Background(), &SystemFeatures{
UserSchema: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "SYSTEM",
},
},
{
name: "push error",
eventstore: expectEventstore(
@ -132,12 +150,17 @@ func TestCommands_SetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemLegacyIntrospectionEventType, true,
),
feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, true,
),
),
),
args: args{context.Background(), &SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: gu.Ptr(true),
UserSchema: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "SYSTEM",
@ -178,12 +201,17 @@ func TestCommands_SetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemTriggerIntrospectionProjectionsEventType, false,
),
feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, true,
),
),
),
args: args{context.Background(), &SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: gu.Ptr(true),
UserSchema: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "SYSTEM",

View File

@ -0,0 +1,180 @@
package command
import (
"bytes"
"context"
"encoding/json"
"github.com/zitadel/zitadel/internal/domain"
domain_schema "github.com/zitadel/zitadel/internal/domain/schema"
"github.com/zitadel/zitadel/internal/repository/user/schema"
"github.com/zitadel/zitadel/internal/zerrors"
)
type CreateUserSchema struct {
ResourceOwner string
Type string
Schema json.RawMessage
PossibleAuthenticators []domain.AuthenticatorType
}
func (s *CreateUserSchema) Valid() error {
if s.Type == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMA-DGFj3", "Errors.UserSchema.Type.Missing")
}
if err := validateUserSchema(s.Schema); err != nil {
return err
}
for _, authenticator := range s.PossibleAuthenticators {
if authenticator == domain.AuthenticatorTypeUnspecified {
return zerrors.ThrowInvalidArgument(nil, "COMMA-Gh652", "Errors.UserSchema.Authenticator.Invalid")
}
}
return nil
}
type UpdateUserSchema struct {
ID string
ResourceOwner string
Type *string
Schema json.RawMessage
PossibleAuthenticators []domain.AuthenticatorType
}
func (s *UpdateUserSchema) Valid() error {
if s.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMA-H5421", "Errors.IDMissing")
}
if s.Type != nil && *s.Type == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMA-G43gn", "Errors.UserSchema.Type.Missing")
}
if err := validateUserSchema(s.Schema); err != nil {
return err
}
for _, authenticator := range s.PossibleAuthenticators {
if authenticator == domain.AuthenticatorTypeUnspecified {
return zerrors.ThrowInvalidArgument(nil, "COMMA-WF4hg", "Errors.UserSchema.Authenticator.Invalid")
}
}
return nil
}
func (c *Commands) CreateUserSchema(ctx context.Context, userSchema *CreateUserSchema) (string, *domain.ObjectDetails, error) {
if err := userSchema.Valid(); err != nil {
return "", nil, err
}
if userSchema.ResourceOwner == "" {
return "", nil, zerrors.ThrowInvalidArgument(nil, "COMMA-J3hhj", "Errors.ResourceOwnerMissing")
}
id, err := c.idGenerator.Next()
if err != nil {
return "", nil, err
}
writeModel := NewUserSchemaWriteModel(id, userSchema.ResourceOwner)
err = c.pushAppendAndReduce(ctx, writeModel,
schema.NewCreatedEvent(ctx,
UserSchemaAggregateFromWriteModel(&writeModel.WriteModel),
userSchema.Type, userSchema.Schema, userSchema.PossibleAuthenticators,
),
)
if err != nil {
return "", nil, err
}
return id, writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) UpdateUserSchema(ctx context.Context, userSchema *UpdateUserSchema) (*domain.ObjectDetails, error) {
if err := userSchema.Valid(); err != nil {
return nil, err
}
writeModel := NewUserSchemaWriteModel(userSchema.ID, userSchema.ResourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
if writeModel.State != domain.UserSchemaStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-HB3e1", "Errors.UserSchema.NotActive")
}
updatedEvent := writeModel.NewUpdatedEvent(
ctx,
UserSchemaAggregateFromWriteModel(&writeModel.WriteModel),
userSchema.Type,
userSchema.Schema,
userSchema.PossibleAuthenticators,
)
if updatedEvent == nil {
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
if err := c.pushAppendAndReduce(ctx, writeModel, updatedEvent); err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) DeactivateUserSchema(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-Vvf3w", "Errors.IDMissing")
}
writeModel := NewUserSchemaWriteModel(id, resourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
if writeModel.State != domain.UserSchemaStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-E4t4z", "Errors.UserSchema.NotActive")
}
err := c.pushAppendAndReduce(ctx, writeModel,
schema.NewDeactivatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel)),
)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) ReactivateUserSchema(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-wq3Gw", "Errors.IDMissing")
}
writeModel := NewUserSchemaWriteModel(id, resourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
if writeModel.State != domain.UserSchemaStateInactive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-DGzh5", "Errors.UserSchema.NotInactive")
}
err := c.pushAppendAndReduce(ctx, writeModel,
schema.NewReactivatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel)),
)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func (c *Commands) DeleteUserSchema(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) {
if id == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-E22gg", "Errors.IDMissing")
}
writeModel := NewUserSchemaWriteModel(id, resourceOwner)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
if !writeModel.Exists() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-Grg41", "Errors.UserSchema.NotExists")
}
// TODO: check for users based on that schema; this is only possible with / after https://github.com/zitadel/zitadel/issues/7308
err := c.pushAppendAndReduce(ctx, writeModel,
schema.NewDeletedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel), writeModel.SchemaType),
)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&writeModel.WriteModel), nil
}
func validateUserSchema(userSchema json.RawMessage) error {
_, err := domain_schema.NewSchema(0, bytes.NewReader(userSchema))
if err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid")
}
return nil
}

View File

@ -0,0 +1,112 @@
package command
import (
"bytes"
"context"
"encoding/json"
"golang.org/x/exp/slices"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user/schema"
)
type UserSchemaWriteModel struct {
eventstore.WriteModel
SchemaType string
Schema json.RawMessage
PossibleAuthenticators []domain.AuthenticatorType
State domain.UserSchemaState
}
func NewUserSchemaWriteModel(schemaID, resourceOwner string) *UserSchemaWriteModel {
return &UserSchemaWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: schemaID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *UserSchemaWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *schema.CreatedEvent:
wm.SchemaType = e.SchemaType
wm.Schema = e.Schema
wm.PossibleAuthenticators = e.PossibleAuthenticators
wm.State = domain.UserSchemaStateActive
case *schema.UpdatedEvent:
if e.SchemaType != nil {
wm.SchemaType = *e.SchemaType
}
if len(e.Schema) > 0 {
wm.Schema = e.Schema
}
if len(e.PossibleAuthenticators) > 0 {
wm.PossibleAuthenticators = e.PossibleAuthenticators
}
case *schema.DeactivatedEvent:
wm.State = domain.UserSchemaStateInactive
case *schema.ReactivatedEvent:
wm.State = domain.UserSchemaStateActive
case *schema.DeletedEvent:
wm.State = domain.UserSchemaStateDeleted
}
}
return wm.WriteModel.Reduce()
}
func (wm *UserSchemaWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(schema.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
schema.CreatedType,
schema.UpdatedType,
schema.DeactivatedType,
schema.ReactivatedType,
schema.DeletedType,
).
Builder()
}
func (wm *UserSchemaWriteModel) NewUpdatedEvent(
ctx context.Context,
agg *eventstore.Aggregate,
schemaType *string,
userSchema json.RawMessage,
possibleAuthenticators []domain.AuthenticatorType,
) *schema.UpdatedEvent {
changes := make([]schema.Changes, 0)
if schemaType != nil && wm.SchemaType != *schemaType {
changes = append(changes, schema.ChangeSchemaType(wm.SchemaType, *schemaType))
}
if !bytes.Equal(wm.Schema, userSchema) {
changes = append(changes, schema.ChangeSchema(userSchema))
}
if len(possibleAuthenticators) > 0 && slices.Compare(wm.PossibleAuthenticators, possibleAuthenticators) != 0 {
changes = append(changes, schema.ChangePossibleAuthenticators(possibleAuthenticators))
}
if len(changes) == 0 {
return nil
}
return schema.NewUpdatedEvent(ctx, agg, changes)
}
func UserSchemaAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return &eventstore.Aggregate{
ID: wm.AggregateID,
Type: schema.AggregateType,
ResourceOwner: wm.ResourceOwner,
InstanceID: wm.InstanceID,
Version: schema.AggregateVersion,
}
}
func (wm *UserSchemaWriteModel) Exists() bool {
return wm.State != domain.UserSchemaStateUnspecified && wm.State != domain.UserSchemaStateDeleted
}

View File

@ -0,0 +1,912 @@
package command
import (
"context"
"encoding/json"
"testing"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/user/schema"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestCommands_CreateUserSchema(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
ctx context.Context
userSchema *CreateUserSchema
}
type res struct {
id string
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"no type, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-DGFj3", "Errors.UserSchema.Type.Missing"),
},
},
{
"no schema, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
Type: "type",
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
},
},
{
"invalid authenticator, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
Type: "type",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUnspecified,
},
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-Gh652", "Errors.UserSchema.Authenticator.Invalid"),
},
},
{
"no resourceOwner, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
Type: "type",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-J3hhj", "Errors.ResourceOwnerMissing"),
},
},
{
"empty user schema created",
fields{
eventstore: expectEventstore(
expectPush(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
idGenerator: mock.ExpectID(t, "id1"),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
ResourceOwner: "instanceID",
Type: "type",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
{
"user schema created",
fields{
eventstore: expectEventstore(
expectPush(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
idGenerator: mock.ExpectID(t, "id1"),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
ResourceOwner: "instanceID",
Type: "type",
Schema: json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
{
"user schema with invalid permission, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
ResourceOwner: "instanceID",
Type: "type",
Schema: json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": true
}
}
}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
},
},
{
"user schema with permission created",
fields{
eventstore: expectEventstore(
expectPush(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
}
}
}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
idGenerator: mock.ExpectID(t, "id1"),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &CreateUserSchema{
ResourceOwner: "instanceID",
Type: "type",
Schema: json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
}
}
}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
gotID, gotDetails, err := c.CreateUserSchema(tt.args.ctx, tt.args.userSchema)
assert.Equal(t, tt.res.id, gotID)
assert.Equal(t, tt.res.details, gotDetails)
assert.ErrorIs(t, err, tt.res.err)
})
}
}
func TestCommands_UpdateUserSchema(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
userSchema *UpdateUserSchema
}
type res struct {
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"missing id, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-H5421", "Errors.IDMissing"),
},
},
{
"empty type, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Type: gu.Ptr(""),
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-G43gn", "Errors.UserSchema.Type.Missing"),
},
},
{
"no schema, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
},
},
{
"invalid schema, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Schema: json.RawMessage(`{
"properties": {
"name": {
"type": "string",
"required": true,
}
}
}`),
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
},
},
{
"invalid authenticator, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUnspecified,
},
},
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-WF4hg", "Errors.UserSchema.Authenticator.Invalid"),
},
},
{
"not active / exists, error",
fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Type: gu.Ptr("type"),
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMA-HB3e1", "Errors.UserSchema.NotActive"),
},
},
{
"no changes",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Type: gu.Ptr("type"),
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
},
},
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
{
"update type",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
expectPush(
schema.NewUpdatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
[]schema.Changes{schema.ChangeSchemaType("type", "newType")},
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Schema: json.RawMessage(`{}`),
Type: gu.Ptr("newType"),
},
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
{
"update schema",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
}
}
}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
expectPush(
schema.NewUpdatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
[]schema.Changes{schema.ChangeSchema(json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
},
"description": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
}
}
}`))},
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Schema: json.RawMessage(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
},
"description": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "rw"
}
}
}
}`),
Type: nil,
},
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
{
"update possible authenticators",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
expectPush(
schema.NewUpdatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
[]schema.Changes{schema.ChangePossibleAuthenticators([]domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
domain.AuthenticatorTypePassword,
})},
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
userSchema: &UpdateUserSchema{
ID: "id1",
Schema: json.RawMessage(`{}`),
PossibleAuthenticators: []domain.AuthenticatorType{
domain.AuthenticatorTypeUsername,
domain.AuthenticatorTypePassword,
},
},
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
}
got, err := c.UpdateUserSchema(tt.args.ctx, tt.args.userSchema)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.details, got)
})
}
}
func TestCommands_DeactivateUserSchema(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
id string
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"missing id, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "",
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-Vvf3w", "Errors.IDMissing"),
},
},
{
"not active / exists, error",
fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMA-E4t4z", "Errors.UserSchema.NotActive"),
},
},
{
"deactivate ok",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
expectPush(
schema.NewDeactivatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
}
got, err := c.DeactivateUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.details, got)
})
}
}
func TestCommands_ReactivateUserSchema(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
id string
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"missing id, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "",
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-wq3Gw", "Errors.IDMissing"),
},
},
{
"not deactivated / exists, error",
fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMA-DGzh5", "Errors.UserSchema.NotInactive"),
},
},
{
"reactivate ok",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
eventFromEventPusher(
schema.NewDeactivatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
),
),
),
expectPush(
schema.NewReactivatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
}
got, err := c.ReactivateUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.details, got)
})
}
}
func TestCommands_DeleteUserSchema(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
id string
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"missing id, error",
fields{
eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "",
},
res{
err: zerrors.ThrowInvalidArgument(nil, "COMMA-E22gg", "Errors.IDMissing"),
},
},
{
"not exists, error",
fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMA-Grg41", "Errors.UserSchema.NotExists"),
},
},
{
"delete ok",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
schema.NewCreatedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
json.RawMessage(`{}`),
[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
),
),
),
expectPush(
schema.NewDeletedEvent(
context.Background(),
&schema.NewAggregate("id1", "instanceID").Aggregate,
"type",
),
),
),
},
args{
ctx: authz.NewMockContext("instanceID", "", ""),
id: "id1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "instanceID",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
}
got, err := c.DeleteUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
assert.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.details, got)
})
}
}

View File

@ -0,0 +1,120 @@
package schema
import (
_ "embed"
"github.com/santhosh-tekuri/jsonschema/v5"
"github.com/zitadel/zitadel/internal/zerrors"
)
var (
//go:embed permission.schema.v1.json
permissionJSON string
permissionSchema = jsonschema.MustCompileString(PermissionSchemaID, permissionJSON)
)
const (
PermissionSchemaID = "urn:zitadel:schema:permission-schema:v1"
PermissionProperty = "urn:zitadel:schema:permission"
)
type role int32
const (
roleUnspecified role = iota
roleSelf
roleOwner
)
type permissionExtension struct {
role role
}
// Compile implements the [jsonschema.ExtCompiler] interface.
// It parses the permission schema extension / annotation of the passed field.
func (c permissionExtension) Compile(ctx jsonschema.CompilerContext, m map[string]interface{}) (_ jsonschema.ExtSchema, err error) {
perm, ok := m[PermissionProperty]
if !ok {
return nil, nil
}
p, ok := perm.(map[string]interface{})
if !ok {
return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-WR5gs", "invalid permission")
}
perms := new(permissions)
for key, value := range p {
switch key {
case "self":
perms.self, err = mapPermission(value)
if err != nil {
return
}
case "owner":
perms.owner, err = mapPermission(value)
if err != nil {
return
}
default:
return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission role")
}
}
return permissionExtensionConfig{c.role, perms}, nil
}
type permissionExtensionConfig struct {
role role
permissions *permissions
}
// Validate implements the [jsonschema.ExtSchema] interface.
// It validates the fields of the json instance according to the permission schema.
func (s permissionExtensionConfig) Validate(ctx jsonschema.ValidationContext, v interface{}) error {
switch s.role {
case roleSelf:
if s.permissions.self == nil || !s.permissions.self.write {
return ctx.Error("permission", "missing required permission")
}
return nil
case roleOwner:
if s.permissions.owner == nil || !s.permissions.owner.write {
return ctx.Error("permission", "missing required permission")
}
return nil
case roleUnspecified:
fallthrough
default:
return ctx.Error("permission", "missing required permission")
}
}
func mapPermission(value any) (*permission, error) {
p := new(permission)
switch v := value.(type) {
case string:
for _, s := range v {
switch s {
case 'r':
p.read = true
case 'w':
p.write = true
default:
return nil, zerrors.ThrowInvalidArgumentf(nil, "SCHEMA-EZ5zjh", "invalid permission pattern: `%s` in (%s)", string(s), v)
}
}
return p, nil
default:
return nil, zerrors.ThrowInvalidArgumentf(nil, "SCHEMA-E5h31", "invalid permission type %T (%v)", v, v)
}
}
type permissions struct {
self *permission
owner *permission
}
type permission struct {
read bool
write bool
}

View File

@ -0,0 +1,28 @@
{
"$id": "urn:zitadel:schema:permission-schema:v1",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"urn:zitadel:schema:property-permission": {
"oneOf": [
{
"type": "string",
"pattern": "^[rw]$"
}
]
}
},
"properties": {
"urn:zitadel:schema:permission": {
"type": "object",
"additionalProperties": false,
"properties": {
"owner": {
"$ref": "#/$defs/urn:zitadel:schema:property-permission"
},
"self": {
"$ref": "#/$defs/urn:zitadel:schema:property-permission"
}
}
}
}
}

View File

@ -0,0 +1,253 @@
package schema
import (
_ "embed"
"encoding/json"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestPermissionExtension(t *testing.T) {
type args struct {
role role
schema string
instance string
}
type want struct {
compilationErr error
validationErr bool
}
tests := []struct {
name string
args args
want want
}{
{
"invalid permission, compilation err",
args{
schema: `{
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": "read"
}
}
}`,
},
want{
compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-WR5gs", "invalid permission"),
},
},
{
"invalid permission string, compilation err",
args{
schema: `{
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"self": "read"
}
}
}
}`,
},
want{
compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-EZ5zjh", "invalid permission pattern: `e` in (read)"),
},
},
{
"invalid permission type, compilation err",
args{
schema: `{
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"owner": true
}
}
}
}`,
},
want{
compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-E5h31", "invalid permission type bool (true)"),
},
},
{
"invalid role, compilation err",
args{
schema: `{
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"IAM_OWNER": "rw"
}
}
}
}`,
},
want{
compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission role"),
},
},
{
"invalid permission self, validation err",
args{
role: roleSelf,
schema: `{
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"owner": "rw",
"self": "r"
}
}
}
}`,
instance: `{ "name": "test"}`,
},
want{
validationErr: true,
},
},
{
"invalid permission owner, validation err",
args{
role: roleOwner,
schema: `{
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"owner": "r",
"self": "r"
}
}
}
}`,
instance: `{ "name": "test"}`,
},
want{
validationErr: true,
},
},
{
"valid permission self, ok",
args{
role: roleSelf,
schema: `{
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"owner": "r",
"self": "rw"
}
}
}
}`,
instance: `{ "name": "test"}`,
},
want{
validationErr: false,
},
},
{
"valid permission owner, ok",
args{
role: roleOwner,
schema: `{
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"owner": "rw",
"self": "r"
}
}
}
}`,
instance: `{ "name": "test"}`,
},
want{
validationErr: false,
},
},
{
"no role, validation err",
args{
role: roleUnspecified,
schema: `{
"type": "object",
"properties": {
"name": {
"type": "string",
"urn:zitadel:schema:permission": {
"owner": "rw",
"self": "rw"
}
}
}
}`,
instance: `{ "name": "test"}`,
},
want{
validationErr: true,
},
},
{
"no permission required, ok",
args{
role: roleSelf,
schema: `{
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}`,
instance: `{ "name": "test"}`,
},
want{
validationErr: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
schema, err := NewSchema(tt.args.role, strings.NewReader(tt.args.schema))
require.ErrorIs(t, err, tt.want.compilationErr)
if tt.want.compilationErr != nil {
return
}
var v interface{}
err = json.Unmarshal([]byte(tt.args.instance), &v)
require.NoError(t, err)
err = schema.Validate(v)
if tt.want.validationErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}

View File

@ -0,0 +1,41 @@
package schema
import (
_ "embed"
"io"
"strings"
"github.com/santhosh-tekuri/jsonschema/v5"
"github.com/zitadel/zitadel/internal/zerrors"
)
var (
//go:embed zitadel.schema.v1.json
zitadelJSON string
)
const (
MetaSchemaID = "urn:zitadel:schema:v1"
)
func NewSchema(role role, r io.Reader) (*jsonschema.Schema, error) {
c := jsonschema.NewCompiler()
if err := c.AddResource(PermissionSchemaID, strings.NewReader(permissionJSON)); err != nil {
return nil, err
}
if err := c.AddResource(MetaSchemaID, strings.NewReader(zitadelJSON)); err != nil {
return nil, err
}
c.RegisterExtension(PermissionSchemaID, permissionSchema, permissionExtension{
role,
})
if err := c.AddResource("schema.json", r); err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "COMMA-Frh42", "Errors.UserSchema.Schema.Invalid")
}
schema, err := c.Compile("schema.json")
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid")
}
return schema, nil
}

View File

@ -0,0 +1,13 @@
{
"$id": "urn:zitadel:schema:v1",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"allOf": [
{
"$ref": "https://json-schema.org/draft/2020-12/schema"
},
{
"$ref": "urn:zitadel:schema:permission-schema:v1"
}
]
}

View File

@ -0,0 +1,26 @@
package domain
type UserSchemaState int32
const (
UserSchemaStateUnspecified UserSchemaState = iota
UserSchemaStateActive
UserSchemaStateInactive
UserSchemaStateDeleted
userSchemaStateCount
)
type AuthenticatorType int32
const (
AuthenticatorTypeUnspecified AuthenticatorType = iota
AuthenticatorTypeUsername
AuthenticatorTypePassword
AuthenticatorTypeWebAuthN
AuthenticatorTypeTOTP
AuthenticatorTypeOTPEmail
AuthenticatorTypeOTPSMS
AuthenticatorTypeAuthenticationKey
AuthenticatorTypeIdentityProvider
authenticatorTypeCount
)

View File

@ -8,6 +8,7 @@ const (
KeyLoginDefaultOrg
KeyTriggerIntrospectionProjections
KeyLegacyIntrospection
KeyUserSchema
)
//go:generate enumer -type Level -transform snake -trimprefix Level
@ -27,4 +28,5 @@ type Features struct {
LoginDefaultOrg bool `json:"login_default_org,omitempty"`
TriggerIntrospectionProjections bool `json:"trigger_introspection_projections,omitempty"`
LegacyIntrospection bool `json:"legacy_introspection,omitempty"`
UserSchema bool `json:"user_schema,omitempty"`
}

View File

@ -7,11 +7,11 @@ import (
"strings"
)
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspection"
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schema"
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81}
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92}
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspection"
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schema"
func (i Key) String() string {
if i < 0 || i >= Key(len(_KeyIndex)-1) {
@ -28,9 +28,10 @@ func _KeyNoOp() {
_ = x[KeyLoginDefaultOrg-(1)]
_ = x[KeyTriggerIntrospectionProjections-(2)]
_ = x[KeyLegacyIntrospection-(3)]
_ = x[KeyUserSchema-(4)]
}
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection}
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema}
var _KeyNameToValueMap = map[string]Key{
_KeyName[0:11]: KeyUnspecified,
@ -41,6 +42,8 @@ var _KeyNameToValueMap = map[string]Key{
_KeyLowerName[28:61]: KeyTriggerIntrospectionProjections,
_KeyName[61:81]: KeyLegacyIntrospection,
_KeyLowerName[61:81]: KeyLegacyIntrospection,
_KeyName[81:92]: KeyUserSchema,
_KeyLowerName[81:92]: KeyUserSchema,
}
var _KeyNames = []string{
@ -48,6 +51,7 @@ var _KeyNames = []string{
_KeyName[11:28],
_KeyName[28:61],
_KeyName[61:81],
_KeyName[81:92],
}
// KeyString retrieves an enum value from the enum constants string name.

View File

@ -16,6 +16,7 @@ import (
"golang.org/x/text/language"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/structpb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
@ -37,38 +38,41 @@ import (
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/system"
user_pb "github.com/zitadel/zitadel/pkg/grpc/user"
schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
type Client struct {
CC *grpc.ClientConn
Admin admin.AdminServiceClient
Mgmt mgmt.ManagementServiceClient
Auth auth.AuthServiceClient
UserV2 user.UserServiceClient
SessionV2 session.SessionServiceClient
SettingsV2 settings.SettingsServiceClient
OIDCv2 oidc_pb.OIDCServiceClient
OrgV2 organisation.OrganizationServiceClient
System system.SystemServiceClient
ExecutionV3 execution.ExecutionServiceClient
FeatureV2 feature.FeatureServiceClient
CC *grpc.ClientConn
Admin admin.AdminServiceClient
Mgmt mgmt.ManagementServiceClient
Auth auth.AuthServiceClient
UserV2 user.UserServiceClient
SessionV2 session.SessionServiceClient
SettingsV2 settings.SettingsServiceClient
OIDCv2 oidc_pb.OIDCServiceClient
OrgV2 organisation.OrganizationServiceClient
System system.SystemServiceClient
ExecutionV3 execution.ExecutionServiceClient
FeatureV2 feature.FeatureServiceClient
UserSchemaV3 schema.UserSchemaServiceClient
}
func newClient(cc *grpc.ClientConn) Client {
return Client{
CC: cc,
Admin: admin.NewAdminServiceClient(cc),
Mgmt: mgmt.NewManagementServiceClient(cc),
Auth: auth.NewAuthServiceClient(cc),
UserV2: user.NewUserServiceClient(cc),
SessionV2: session.NewSessionServiceClient(cc),
SettingsV2: settings.NewSettingsServiceClient(cc),
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
OrgV2: organisation.NewOrganizationServiceClient(cc),
System: system.NewSystemServiceClient(cc),
ExecutionV3: execution.NewExecutionServiceClient(cc),
FeatureV2: feature.NewFeatureServiceClient(cc),
CC: cc,
Admin: admin.NewAdminServiceClient(cc),
Mgmt: mgmt.NewManagementServiceClient(cc),
Auth: auth.NewAuthServiceClient(cc),
UserV2: user.NewUserServiceClient(cc),
SessionV2: session.NewSessionServiceClient(cc),
SettingsV2: settings.NewSettingsServiceClient(cc),
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
OrgV2: organisation.NewOrganizationServiceClient(cc),
System: system.NewSystemServiceClient(cc),
ExecutionV3: execution.NewExecutionServiceClient(cc),
FeatureV2: feature.NewFeatureServiceClient(cc),
UserSchemaV3: schema.NewUserSchemaServiceClient(cc),
}
}
@ -540,3 +544,21 @@ func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *execution
require.NoError(t, err)
return target
}
func (s *Tester) CreateUserSchema(ctx context.Context, t *testing.T) *schema.CreateUserSchemaResponse {
userSchema := new(structpb.Struct)
err := userSchema.UnmarshalJSON([]byte(`{
"$schema": "urn:zitadel:schema:v1",
"type": "object",
"properties": {}
}`))
require.NoError(t, err)
target, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &schema.CreateUserSchemaRequest{
Type: fmt.Sprint(time.Now().UnixNano() + 1),
DataType: &schema.CreateUserSchemaRequest_Schema{
Schema: userSchema,
},
})
require.NoError(t, err)
return target
}

View File

@ -11,6 +11,7 @@ type InstanceFeatures struct {
LoginDefaultOrg FeatureSource[bool]
TriggerIntrospectionProjections FeatureSource[bool]
LegacyIntrospection FeatureSource[bool]
UserSchema FeatureSource[bool]
}
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {

View File

@ -60,6 +60,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceLoginDefaultOrgEventType,
feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
feature_v2.InstanceLegacyIntrospectionEventType,
feature_v2.InstanceUserSchemaEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@ -71,6 +72,7 @@ func (m *InstanceFeaturesReadModel) reduceReset() {
m.instance.LoginDefaultOrg = FeatureSource[bool]{}
m.instance.TriggerIntrospectionProjections = FeatureSource[bool]{}
m.instance.LegacyIntrospection = FeatureSource[bool]{}
m.instance.UserSchema = FeatureSource[bool]{}
}
func (m *InstanceFeaturesReadModel) populateFromSystem() bool {
@ -80,6 +82,7 @@ func (m *InstanceFeaturesReadModel) populateFromSystem() bool {
m.instance.LoginDefaultOrg = m.system.LoginDefaultOrg
m.instance.TriggerIntrospectionProjections = m.system.TriggerIntrospectionProjections
m.instance.LegacyIntrospection = m.system.LegacyIntrospection
m.instance.UserSchema = m.system.UserSchema
return true
}
@ -99,6 +102,8 @@ func (m *InstanceFeaturesReadModel) reduceBoolFeature(event *feature_v2.SetEvent
dst = &m.instance.TriggerIntrospectionProjections
case feature.KeyLegacyIntrospection:
dst = &m.instance.LegacyIntrospection
case feature.KeyUserSchema:
dst = &m.instance.UserSchema
}
*dst = FeatureSource[bool]{
Level: level,

View File

@ -101,6 +101,10 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent[bool](
ctx, aggregate,
feature_v2.InstanceUserSchemaEventType, false,
)),
),
),
args: args{true},
@ -120,6 +124,10 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
Level: feature.LevelInstance,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelInstance,
Value: false,
},
},
},
{
@ -142,6 +150,10 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent[bool](
ctx, aggregate,
feature_v2.InstanceUserSchemaEventType, false,
)),
eventFromEventPusher(feature_v2.NewResetEvent(
ctx, aggregate,
feature_v2.InstanceResetEventType,
@ -169,6 +181,10 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
Level: feature.LevelUnspecified,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
},
},
{
@ -187,6 +203,10 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent[bool](
ctx, aggregate,
feature_v2.InstanceUserSchemaEventType, false,
)),
eventFromEventPusher(feature_v2.NewResetEvent(
ctx, aggregate,
feature_v2.InstanceResetEventType,
@ -214,6 +234,10 @@ func TestQueries_GetInstanceFeatures(t *testing.T) {
Level: feature.LevelUnspecified,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
},
},
}

View File

@ -71,6 +71,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.InstanceLegacyIntrospectionEventType,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: feature_v2.InstanceUserSchemaEventType,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),

View File

@ -63,6 +63,10 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.SystemLegacyIntrospectionEventType,
Reduce: reduceSystemSetFeature[bool],
},
{
Event: feature_v2.SystemUserSchemaEventType,
Reduce: reduceSystemSetFeature[bool],
},
},
}}
}

View File

@ -18,6 +18,7 @@ type SystemFeatures struct {
LoginDefaultOrg FeatureSource[bool]
TriggerIntrospectionProjections FeatureSource[bool]
LegacyIntrospection FeatureSource[bool]
UserSchema FeatureSource[bool]
}
func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) {

View File

@ -48,6 +48,7 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemLoginDefaultOrgEventType,
feature_v2.SystemTriggerIntrospectionProjectionsEventType,
feature_v2.SystemLegacyIntrospectionEventType,
feature_v2.SystemUserSchemaEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@ -72,6 +73,8 @@ func (m *SystemFeaturesReadModel) reduceBoolFeature(event *feature_v2.SetEvent[b
dst = &m.system.TriggerIntrospectionProjections
case feature.KeyLegacyIntrospection:
dst = &m.system.LegacyIntrospection
case feature.KeyUserSchema:
dst = &m.system.UserSchema
}
*dst = FeatureSource[bool]{

View File

@ -57,6 +57,10 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, false,
)),
),
),
want: &SystemFeatures{
@ -75,6 +79,10 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
Level: feature.LevelSystem,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelSystem,
Value: false,
},
},
},
{
@ -93,6 +101,10 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, false,
)),
eventFromEventPusher(feature_v2.NewResetEvent(
context.Background(), aggregate,
feature_v2.SystemResetEventType,
@ -119,6 +131,10 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
Level: feature.LevelUnspecified,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
},
},
{
@ -137,6 +153,10 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemLegacyIntrospectionEventType, false,
)),
eventFromEventPusher(feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.SystemUserSchemaEventType, false,
)),
eventFromEventPusher(feature_v2.NewResetEvent(
context.Background(), aggregate,
feature_v2.SystemResetEventType,
@ -163,6 +183,10 @@ func TestQueries_GetSystemFeatures(t *testing.T) {
Level: feature.LevelUnspecified,
Value: false,
},
UserSchema: FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
},
},
}

View File

@ -9,8 +9,10 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, SystemLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, SystemTriggerIntrospectionProjectionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, SystemLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, SystemUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceResetEventType, eventstore.GenericEventMapper[ResetEvent])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceTriggerIntrospectionProjectionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]])
}

View File

@ -15,11 +15,13 @@ var (
SystemLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLoginDefaultOrg)
SystemTriggerIntrospectionProjectionsEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTriggerIntrospectionProjections)
SystemLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLegacyIntrospection)
SystemUserSchemaEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyUserSchema)
InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance)
InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg)
InstanceTriggerIntrospectionProjectionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTriggerIntrospectionProjections)
InstanceLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLegacyIntrospection)
InstanceUserSchemaEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyUserSchema)
)
const (

View File

@ -0,0 +1,25 @@
package schema
import (
"github.com/zitadel/zitadel/internal/eventstore"
)
const (
AggregateType = "user_schema"
AggregateVersion = "v1"
)
type Aggregate struct {
eventstore.Aggregate
}
func NewAggregate(id, resourceOwner string) *Aggregate {
return &Aggregate{
Aggregate: eventstore.Aggregate{
Type: AggregateType,
Version: AggregateVersion,
ID: id,
ResourceOwner: resourceOwner,
},
}
}

View File

@ -0,0 +1,11 @@
package schema
import "github.com/zitadel/zitadel/internal/eventstore"
func init() {
eventstore.RegisterFilterEventMapper(AggregateType, CreatedType, eventstore.GenericEventMapper[CreatedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, UpdatedType, eventstore.GenericEventMapper[UpdatedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, DeactivatedType, eventstore.GenericEventMapper[DeactivatedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, ReactivatedType, eventstore.GenericEventMapper[ReactivatedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, DeletedType, eventstore.GenericEventMapper[DeletedEvent])
}

View File

@ -0,0 +1,233 @@
package schema
import (
"context"
"encoding/json"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
)
const (
eventPrefix = "user_schema."
CreatedType = eventPrefix + "created"
UpdatedType = eventPrefix + "updated"
DeactivatedType = eventPrefix + "deactivated"
ReactivatedType = eventPrefix + "reactivated"
DeletedType = eventPrefix + "deleted"
uniqueSchemaType = "user_schema_type"
)
func NewAddSchemaTypeUniqueConstraint(schemaType string) *eventstore.UniqueConstraint {
return eventstore.NewAddEventUniqueConstraint(
uniqueSchemaType,
schemaType,
"Errors.UserSchema.Type.AlreadyExists")
}
func NewRemoveSchemaTypeUniqueConstraint(schemaType string) *eventstore.UniqueConstraint {
return eventstore.NewRemoveUniqueConstraint(
uniqueSchemaType,
schemaType,
)
}
type CreatedEvent struct {
*eventstore.BaseEvent `json:"-"`
SchemaType string `json:"schemaType"`
Schema json.RawMessage `json:"schema,omitempty"`
PossibleAuthenticators []domain.AuthenticatorType `json:"possibleAuthenticators,omitempty"`
}
func (e *CreatedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = event
}
func (e *CreatedEvent) Payload() interface{} {
return e
}
func (e *CreatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{NewAddSchemaTypeUniqueConstraint(e.SchemaType)}
}
func NewCreatedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
schemaType string,
schema json.RawMessage,
possibleAuthenticators []domain.AuthenticatorType,
) *CreatedEvent {
return &CreatedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
CreatedType,
),
SchemaType: schemaType,
Schema: schema,
PossibleAuthenticators: possibleAuthenticators,
}
}
type UpdatedEvent struct {
*eventstore.BaseEvent `json:"-"`
SchemaType *string `json:"schemaType,omitempty"`
Schema json.RawMessage `json:"schema,omitempty"`
PossibleAuthenticators []domain.AuthenticatorType `json:"possibleAuthenticators,omitempty"`
oldSchemaType string
}
func (e *UpdatedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = event
}
func (e *UpdatedEvent) Payload() interface{} {
return e
}
func (e *UpdatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
if e.oldSchemaType == "" {
return nil
}
return []*eventstore.UniqueConstraint{
NewRemoveSchemaTypeUniqueConstraint(e.oldSchemaType),
NewAddSchemaTypeUniqueConstraint(*e.SchemaType),
}
}
func NewUpdatedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
changes []Changes,
) *UpdatedEvent {
updatedEvent := &UpdatedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
UpdatedType,
),
}
for _, change := range changes {
change(updatedEvent)
}
return updatedEvent
}
type Changes func(event *UpdatedEvent)
func ChangeSchemaType(oldSchemaType, schemaType string) func(event *UpdatedEvent) {
return func(e *UpdatedEvent) {
e.SchemaType = &schemaType
e.oldSchemaType = oldSchemaType
}
}
func ChangeSchema(schema json.RawMessage) func(event *UpdatedEvent) {
return func(e *UpdatedEvent) {
e.Schema = schema
}
}
func ChangePossibleAuthenticators(possibleAuthenticators []domain.AuthenticatorType) func(event *UpdatedEvent) {
return func(e *UpdatedEvent) {
e.PossibleAuthenticators = possibleAuthenticators
}
}
type DeactivatedEvent struct {
*eventstore.BaseEvent `json:"-"`
}
func (e *DeactivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = event
}
func (e *DeactivatedEvent) Payload() interface{} {
return e
}
func (e *DeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewDeactivatedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *DeactivatedEvent {
return &DeactivatedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
DeactivatedType,
),
}
}
type ReactivatedEvent struct {
*eventstore.BaseEvent `json:"-"`
}
func (e *ReactivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = event
}
func (e *ReactivatedEvent) Payload() interface{} {
return e
}
func (e *ReactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewReactivatedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *ReactivatedEvent {
return &ReactivatedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
ReactivatedType,
),
}
}
type DeletedEvent struct {
*eventstore.BaseEvent `json:"-"`
schemaType string
}
func (e *DeletedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = event
}
func (e *DeletedEvent) Payload() interface{} {
return e
}
func (e *DeletedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{
NewRemoveSchemaTypeUniqueConstraint(e.schemaType),
}
}
func NewDeletedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
schemaType string,
) *DeletedEvent {
return &DeletedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
DeletedType,
),
schemaType: schemaType,
}
}

View File

@ -563,6 +563,16 @@ Errors:
NotFound: Изпълнението не е намерено
IncludeNotFound: Включването не е намерено
NoTargets: Няма определени цели
UserSchema:
NotEnabled: Функцията „Потребителска схема“ не е активирана
Type:
Missing: Липсва тип потребителска схема
AlreadyExists: Типът потребителска схема вече съществува
Authenticator:
Invalid: Невалиден тип удостоверител
NotActive: Потребителската схема не е активна
NotInactive: Потребителската схема не е неактивна
NotExists: Потребителската схема не съществува
AggregateTypes:
action: Действие
@ -576,6 +586,7 @@ AggregateTypes:
feature: Особеност
target: Целта
execution: Екзекуция
user_schema: Потребителска схема
EventTypes:
execution:
@ -1268,6 +1279,12 @@ EventTypes:
password:
changed: Паролата на SMTP конфигурацията е променена
removed: Премахната SMTP конфигурация
user_schema:
created: Създадена е потребителска схема
updated: Потребителската схема е актуализирана
deactivated: Потребителската схема е деактивирана
reactivated: Потребителската схема е активирана отново
deleted: Потребителската схема е изтрита
Application:
OIDC:
UnsupportedVersion: Вашата OIDC версия не се поддържа

View File

@ -543,6 +543,16 @@ Errors:
NotFound: Provedení nenalezeno
IncludeNotFound: Zahrnout nenalezeno
NoTargets: Nejsou definovány žádné cíle
UserSchema:
NotEnabled: Funkce "Uživatelské schéma" není povolena
Type:
Missing: Chybí typ uživatelského schématu
AlreadyExists: Typ uživatelského schématu již existuje
Authenticator:
Invalid: Neplatný typ ověřovače
NotActive: Uživatelské schéma není aktivní
NotInactive: Uživatelské schéma není neaktivní
NotExists: Uživatelské schéma neexistuje
AggregateTypes:
action: Akce
@ -556,6 +566,7 @@ AggregateTypes:
feature: Funkce
target: Cíl
execution: Provedení
user_schema: Uživatelské schéma
EventTypes:
execution:
@ -1233,6 +1244,12 @@ EventTypes:
password:
changed: Heslo konfigurace SMTP změněno
removed: Konfigurace SMTP odstraněna
user_schema:
created: Vytvořeno uživatelské schéma
updated: Uživatelské schéma bylo aktualizováno
deactivated: Uživatelské schéma deaktivováno
reactivated: Uživatelské schéma bylo znovu aktivováno
deleted: Uživatelské schéma bylo smazáno
Application:
OIDC:

View File

@ -546,6 +546,16 @@ Errors:
NotFound: Ausführung nicht gefunden
IncludeNotFound: Einschließen nicht gefunden
NoTargets: Keine Ziele definiert
UserSchema:
NotEnabled: Funktion Benutzerschema ist nicht aktiviert
Type:
Missing: Benutzerschematyp fehlt
AlreadyExists: Benutzerschematyp existiert bereits
Authenticator:
Invalid: Ungültiger Authentifizierungstyp
NotActive: Benutzerschema nicht aktiv
NotInactive: Benutzerschema nicht inaktiv
NotExists: Benutzerschema existiert nicht
AggregateTypes:
action: Action
@ -559,6 +569,7 @@ AggregateTypes:
feature: Feature
target: Ziel
execution: Ausführung
user_schema: Benutzerschema
EventTypes:
execution:
@ -1236,6 +1247,12 @@ EventTypes:
password:
changed: Passwort von SMTP Konfiguration geändert
removed: SMTP Konfiguration gelöscht
user_schema:
created: Benutzerschema erstellt
updated: Benutzerschema geändert
deactivated: Benutzerschema deaktiviert
reactivated: Benutzerschema reaktiviert
deleted: Benutzerschema gelöscht
Application:
OIDC:

View File

@ -546,6 +546,16 @@ Errors:
NotFound: Execution not found
IncludeNotFound: Include not found
NoTargets: No targets defined
UserSchema:
NotEnabled: Feature "User Schema" is not enabled
Type:
Missing: User Schema Type missing
AlreadyExists: User Schema Type already exists
Authenticator:
Invalid: Invalid authenticator type
NotActive: User Schema not active
NotInactive: User Schema not inactive
NotExists: User Schema does not exist
AggregateTypes:
action: Action
@ -559,6 +569,7 @@ AggregateTypes:
feature: Feature
target: Target
execution: Execution
user_schema: User Schema
EventTypes:
execution:
@ -1236,6 +1247,12 @@ EventTypes:
password:
changed: Password of SMTP configuration changed
removed: SMTP configuration removed
user_schema:
created: User Schema created
updated: User Schema updated
deactivated: User Schema deactivated
reactivated: User Schema reactivated
deleted: User Schema deleted
Application:
OIDC:

View File

@ -546,6 +546,16 @@ Errors:
NotFound: Ejecución no encontrada
IncludeNotFound: Incluir no encontrado
NoTargets: No hay objetivos definidos
UserSchema:
NotEnabled: La función "Esquema de usuario" no está habilitada
Type:
Missing: Falta el tipo de esquema de usuario
AlreadyExists: El tipo de esquema de usuario ya existe
Authenticator:
Invalid: Tipo de autenticador no válido
NotActive: Esquema de usuario no activo
NotInactive: Esquema de usuario no inactivo
NotExists: El esquema de usuario no existe
AggregateTypes:
action: Acción
@ -559,6 +569,7 @@ AggregateTypes:
feature: Característica
target: Objectivo
execution: Ejecución
user_schema: Esquema de usuario
EventTypes:
execution:
@ -1236,6 +1247,12 @@ EventTypes:
password:
changed: Contraseña de configuración SMTP modificada
removed: Configuración SMTP eliminada
user_schema:
created: Esquema de usuario creado
updated: Esquema de usuario actualizado
deactivated: Esquema de usuario desactivado
reactivated: Esquema de usuario reactivado
deleted: Esquema de usuario eliminado
Application:
OIDC:

View File

@ -546,6 +546,16 @@ Errors:
NotFound: Exécution introuvable
IncludeNotFound: Inclure introuvable
NoTargets: Aucune cible définie
UserSchema:
NotEnabled: La fonctionnalité "Schéma utilisateur" n'est pas activée
Type:
Missing: Type de schéma utilisateur manquant
AlreadyExists: Le type de schéma utilisateur existe déjà
Authenticator:
Invalid: Type d'authentificateur invalide
NotActive: Schéma utilisateur non actif
NotInactive: Le schéma utilisateur n'est pas inactif
NotExists: Le schéma utilisateur n'existe pas
AggregateTypes:
action: Action
@ -559,6 +569,7 @@ AggregateTypes:
feature: Fonctionnalité
target: Cible
execution: Exécution
user_schema: Schéma utilisateur
EventTypes:
execution:
@ -1065,6 +1076,12 @@ EventTypes:
deactivated: Action désactivée
reactivated: Action réactivée
removed: Action supprimée
user_schema:
created: Schéma utilisateur créé
updated: Schéma utilisateur mis à jour
deactivated: Schéma utilisateur désactivé
reactivated: Schéma utilisateur réactivé
deleted: Schéma utilisateur supprimé
Application:
OIDC:

View File

@ -547,7 +547,16 @@ Errors:
NotFound: Esecuzione non trovata
IncludeNotFound: Includi non trovato
NoTargets: Nessun obiettivo definito
UserSchema:
NotEnabled: La funzionalità "Schema utente" non è abilitata
Type:
Missing: Tipo di schema utente mancante
AlreadyExists: Il tipo di schema utente esiste già
Authenticator:
Invalid: Tipo di autenticatore non valido
NotActive: Schema utente non attivo
NotInactive: Schema utente non inattivo
NotExists: Lo schema utente non esiste
AggregateTypes:
action: Azione
@ -561,6 +570,7 @@ AggregateTypes:
feature: Funzionalità
target: Bersaglio
execution: Esecuzione
user_schema: Schema utente
EventTypes:
execution:
@ -1067,6 +1077,12 @@ EventTypes:
deactivated: Azione disattivata
reactivated: Azione riattivata
removed: Azione rimossa
user_schema:
created: Schema utente creato
updated: Schema utente aggiornato
deactivated: Schema utente disattivato
reactivated: Schema utente riattivato
deleted: Schema utente eliminato
Application:
OIDC:

View File

@ -535,6 +535,16 @@ Errors:
NotFound: 実行が見つかりませんでした
IncludeNotFound: 見つからないものを含める
NoTargets: ターゲットが定義されていません
UserSchema:
NotEnabled: 機能「ユーザースキーマ」が有効になっていません
Type:
Missing: ユーザースキーマタイプがありません
AlreadyExists: ユーザースキーマタイプはすでに存在します
Authenticator:
Invalid: 無効な認証子のタイプ
NotActive: ユーザースキーマがアクティブではありません
NotInactive: ユーザースキーマが非アクティブではありません
NotExists: ユーザースキーマが存在しません
AggregateTypes:
action: アクション
@ -548,6 +558,7 @@ AggregateTypes:
feature: 特徴
target: 目標
execution: 実行
user_schema: ユーザースキーマ
EventTypes:
execution:
@ -1225,6 +1236,12 @@ EventTypes:
password:
changed: SMTP構成パスワードの変更
removed: SMTP構成の削除
user_schema:
created: ーザースキーマが作成されました
updated: ユーザースキーマが更新されました
deactivated: ユーザースキーマが非アクティブ化されました
reactivated: ユーザースキーマが再アクティブ化されました
deleted: ユーザースキーマが削除されました
Application:
OIDC:

View File

@ -545,6 +545,16 @@ Errors:
NotFound: Извршувањето не е пронајдено
IncludeNotFound: Вклучете не е пронајден
NoTargets: Не се дефинирани цели
UserSchema:
NotEnabled: Функцијата „Корисничка шема“ не е овозможена
Type:
Missing: Недостасува тип на корисничка шема
AlreadyExists: Тип на корисничка шема веќе постои
Authenticator:
Invalid: Неважечки тип на автентикатор
NotActive: Корисничката шема не е активна
NotInactive: Корисничката шема не е неактивна
NotExists: Корисничката шема не постои
AggregateTypes:
action: Акција
@ -558,6 +568,7 @@ AggregateTypes:
feature: Карактеристика
target: Цел
execution: Извршување
user_schema: Корисничка шема
EventTypes:
execution:
@ -1234,6 +1245,12 @@ EventTypes:
password:
changed: Променета лозинка на SMTP конфигурацијата
removed: Отстранета SMTP конфигурација
user_schema:
created: Создадена е корисничка шема
updated: Корисничката шема е ажурирана
deactivated: Корисничката шема е деактивирана
reactivated: Корисничката шема е реактивирана
deleted: Корисничката шема е избришана
Application:
OIDC:

View File

@ -546,6 +546,16 @@ Errors:
NotFound: Uitvoering niet gevonden
IncludeNotFound: Inclusief niet gevonden
NoTargets: Geen doelstellingen gedefinieerd
UserSchema:
NotEnabled: Functie "Gebruikersschema" is niet ingeschakeld
Type:
Missing: Type gebruikersschema ontbreekt
AlreadyExists: Type gebruikersschema bestaat al
Authenticator:
Invalid: Ongeldig authenticatortype
NotActive: Gebruikersschema niet actief
NotInactive: Gebruikersschema niet inactief
NotExists: Gebruikersschema bestaat niet
AggregateTypes:
action: Actie
@ -559,6 +569,7 @@ AggregateTypes:
feature: Functie
target: Doel
execution: Executie
user_schema: Gebruikersschema
EventTypes:
execution:
@ -1236,6 +1247,12 @@ EventTypes:
password:
changed: Wachtwoord van SMTP-configuratie gewijzigd
removed: SMTP-configuratie verwijderd
user_schema:
created: Gebruikersschema gemaakt
updated: Gebruikersschema bijgewerkt
deactivated: Gebruikersschema gedeactiveerd
reactivated: Gebruikersschema opnieuw geactiveerd
deleted: Gebruikersschema verwijderd
Application:
OIDC:

View File

@ -546,6 +546,16 @@ Errors:
NotFound: Nie znaleziono wykonania
IncludeNotFound: Nie znaleziono uwzględnienia
NoTargets: Nie zdefiniowano celów
UserSchema:
NotEnabled: Funkcja „Schemat użytkownika” nie jest włączona
Type:
Missing: Brak typu schematu użytkownika
AlreadyExists: Typ schematu użytkownika już istnieje
Authenticator:
Invalid: Nieprawidłowy typ uwierzytelnienia
NotActive: Schemat użytkownika nieaktywny
NotInactive: Schemat użytkownika nie jest nieaktywny
NotExists: Schemat użytkownika nie istnieje
AggregateTypes:
action: Działanie
@ -559,6 +569,7 @@ AggregateTypes:
feature: Funkcja
target: Cel
execution: Wykonanie
user_schema: Schemat użytkownika
EventTypes:
execution:
@ -1236,6 +1247,12 @@ EventTypes:
password:
changed: Hasło konfiguracji SMTP zmienione
removed: Konfiguracja SMTP usunięta
user_schema:
created: Utworzono schemat użytkownika
updated: Schemat użytkownika zaktualizowany
deactivated: Schemat użytkownika dezaktywowany
reactivated: Schemat użytkownika został ponownie aktywowany
deleted: Schemat użytkownika został usunięty
Application:
OIDC:

View File

@ -540,6 +540,16 @@ Errors:
NotFound: Execução não encontrada
IncludeNotFound: Incluir não encontrado
NoTargets: Nenhuma meta definida
UserSchema:
NotEnabled: O recurso "Esquema do usuário" não está habilitado
Type:
Missing: Tipo de esquema de usuário ausente
AlreadyExists: O tipo de esquema de usuário já existe
Authenticator:
Invalid: Tipo de autenticador inválido
NotActive: Esquema do usuário não ativo
NotInactive: Esquema do usuário não inativo
NotExists: O esquema do usuário não existe
AggregateTypes:
action: Ação
@ -553,6 +563,7 @@ AggregateTypes:
feature: Recurso
target: Objetivo
execution: Execução
user_schema: Esquema do usuário
EventTypes:
execution:
@ -1230,6 +1241,12 @@ EventTypes:
password:
changed: Senha da configuração SMTP alterada
removed: Configuração SMTP removida
user_schema:
created: Esquema de usuário criado
updated: Esquema do usuário atualizado
deactivated: Esquema de usuário desativado
reactivated: Esquema do usuário reativado
deleted: Esquema do usuário excluído
Application:
OIDC:

View File

@ -534,6 +534,16 @@ Errors:
NotFound: Исполнение не найдено
IncludeNotFound: Включить не найдено
NoTargets: Цели не определены
UserSchema:
NotEnabled: Функция «Пользовательская схема» не включена
Type:
Missing: Тип пользовательской схемы отсутствует
AlreadyExists: Тип пользовательской схемы уже существует
Authenticator:
Invalid: Неверный тип аутентификатора
NotActive: Пользовательская схема не активна
NotInactive: Пользовательская схема не неактивна
NotExists: Пользовательская схема не существует
AggregateTypes:
action: Действие
@ -547,6 +557,7 @@ AggregateTypes:
feature: Особенность
target: мишень
execution: Исполнение
user_schema: Пользовательская схема
EventTypes:
execution:
@ -1224,6 +1235,12 @@ EventTypes:
password:
changed: Изменен пароль конфигурации SMTP
removed: Удалена конфигурация SMTP
user_schema:
created: Пользовательская схема создана
updated: Пользовательская схема обновлена
deactivated: Пользовательская схема деактивирована
reactivated: Пользовательская схема повторно активирована
deleted: Пользовательская схема удалена
Application:
OIDC:
UnsupportedVersion: Ваша версия OIDC не поддерживается

View File

@ -546,6 +546,16 @@ Errors:
NotFound: 未找到执行
IncludeNotFound: 包括未找到的内容
NoTargets: 没有定义目标
UserSchema:
NotEnabled: 未启用“用户架构”功能
Type:
Missing: 缺少用户架构类型
AlreadyExists: 用户架构类型已存在
Authenticator:
Invalid: 验证器类型无效
NotActive: 用户架构未激活
NotInactive: 用户架构未处于非活动状态
NotExists: 用户架构不存在
AggregateTypes:
action: 动作
@ -559,6 +569,7 @@ AggregateTypes:
feature: 特征
target:
execution: 执行
user_schema: 用户模式
EventTypes:
execution:
@ -1065,6 +1076,12 @@ EventTypes:
deactivated: 停用动作
reactivated: 启用动作
removed: 删除动作
user_schema:
created: 已创建用户架构
updated: 用户架构已更新
deactivated: 用户架构已停用
reactivated: 用户架构已重新激活
deleted: 用户架构已删除
Application:
OIDC:

View File

@ -23,13 +23,18 @@ message SetInstanceFeaturesRequest{
description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature.";
}
];
optional bool oidc_legacy_introspection = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
}
];
optional bool user_schema = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
}
];
}
message SetInstanceFeaturesResponse {
@ -73,4 +78,11 @@ message GetInstanceFeaturesResponse {
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
}
];
FeatureFlag user_schema = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
}
];
}

View File

@ -31,6 +31,13 @@ message SetSystemFeaturesRequest{
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
}
];
optional bool user_schema = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
}
];
}
message SetSystemFeaturesResponse {
@ -67,4 +74,11 @@ message GetSystemFeaturesResponse {
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
}
];
FeatureFlag user_schema = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
}
];
}

View File

@ -365,7 +365,7 @@ message CreateUserSchemaRequest {
}
// Defines the possible types of authenticators.
repeated AuthenticatorType possible_authenticators = 3 [
(validate.rules).repeated = {unique: true, items: {enum: {defined_only: true}}},
(validate.rules).repeated = {unique: true, items: {enum: {defined_only: true, not_in: [0]}}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"AUTHENTICATOR_TYPE_USERNAME\",\"AUTHENTICATOR_TYPE_PASSWORD\",\"AUTHENTICATOR_TYPE_WEBAUTHN\"]";
}
@ -407,7 +407,7 @@ message UpdateUserSchemaRequest {
//
// Removal of an authenticator does not remove the authenticator on a user.
repeated AuthenticatorType possible_authenticators = 4 [
(validate.rules).repeated = {unique: true, items: {enum: {defined_only: true}}},
(validate.rules).repeated = {unique: true, items: {enum: {defined_only: true, not_in: [0]}}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"AUTHENTICATOR_TYPE_USERNAME\",\"AUTHENTICATOR_TYPE_PASSWORD\",\"AUTHENTICATOR_TYPE_WEBAUTHN\"]";
}