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
61 changed files with 3614 additions and 35 deletions

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: