mirror of
https://github.com/zitadel/zitadel.git
synced 2025-06-04 14:48:20 +00:00
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:
parent
2a39cc16f5
commit
0e181b218c
@ -832,7 +832,7 @@ DefaultInstance:
|
||||
Greeting: Hello {{.DisplayName}},
|
||||
Text: The password of your user has changed. If this change was not done by you, please be advised to immediately reset your password.
|
||||
ButtonText: Login
|
||||
|
||||
|
||||
# Once a feature is set on the instance (true or false), system level feature settings
|
||||
# will be ignored until instance level features are reset.
|
||||
Features:
|
||||
@ -1016,6 +1016,8 @@ InternalAuthZ:
|
||||
- "execution.read"
|
||||
- "execution.write"
|
||||
- "execution.delete"
|
||||
- "userschema.write"
|
||||
- "userschema.delete"
|
||||
- Role: "IAM_OWNER_VIEWER"
|
||||
Permissions:
|
||||
- "iam.read"
|
||||
|
@ -75,6 +75,7 @@ DefaultInstance:
|
||||
LoginDefaultOrg: true
|
||||
LegacyIntrospection: true
|
||||
TriggerIntrospectionProjections: true
|
||||
UserSchema: true
|
||||
Log:
|
||||
Level: info
|
||||
Actions:
|
||||
@ -86,6 +87,7 @@ Actions:
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(true),
|
||||
UserSchema: gu.Ptr(true),
|
||||
})
|
||||
},
|
||||
}, {
|
||||
|
@ -44,6 +44,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/session/v2"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/system"
|
||||
user_schema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/user/schema/v3alpha"
|
||||
user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2"
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
@ -410,6 +411,9 @@ func startAPIs(
|
||||
if err := apis.RegisterService(ctx, execution_v3_alpha.CreateServer(commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, user_schema_v3_alpha.CreateServer(commands, queries)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, config.ExternalDomain, login.IgnoreInstanceEndpoints...)
|
||||
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
|
||||
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))
|
||||
|
135
docs/docs/guides/manage/customize/user-schema.md
Normal file
135
docs/docs/guides/manage/customize/user-schema.md
Normal file
@ -0,0 +1,135 @@
|
||||
---
|
||||
title: User Schema
|
||||
---
|
||||
|
||||
ZITADEL allows you to define schemas for users, based on the [JSON Schema Standard](https://json-schema.org/).
|
||||
This gives you the possibility to define your own data models for your users, validate them based on these definitions
|
||||
and making sure who has access or manipulate information of the user.
|
||||
|
||||
By defining multiple schemas, you can even differentiate between different personas of your organization or application
|
||||
and restrictions, resp. requirements for them to authenticate.
|
||||
|
||||
For example, you could have separate schemas for your employees and your customers. While you might want to make sure that
|
||||
certain data like given name and family name are required for employees, they might be optional for the latter.
|
||||
Or you might want to disable username password authentication for your admins and only allow phishing resistant methods like passkeys,
|
||||
but still allow it for your customers.
|
||||
|
||||
:::info
|
||||
Please be aware that User Schema is in a [preview stage](/support/software-release-cycles-support#preview) not feature complete
|
||||
and therefore not generally available.
|
||||
|
||||
Do not use it for production yet. To test it out, you need to enable the `UserSchema` [feature flag](/apis/resources/feature_service_v2/feature-service).
|
||||
:::
|
||||
|
||||
## Create your first schema
|
||||
|
||||
Let's create the first very simple schema `user`, which defines a `givenName` and `familyName` for a user and allows them to
|
||||
authenticate with username and password.
|
||||
|
||||
We can do so by calling the [create user schema endpoint](/docs/apis/resources/user_schema_service_v3/user-schema-service-create-user-schema)
|
||||
with the following data. Make sure to provide an access_token with an IAM_OWNER role.
|
||||
|
||||
```bash
|
||||
curl -X POST "https://$CUSTOM-DOMAIN/v3alpha/user_schemas" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
--data-raw '{
|
||||
"type": "user",
|
||||
"schema": {
|
||||
"$schema": "urn:zitadel:schema:v1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"givenName": {
|
||||
"type": "string"
|
||||
},
|
||||
"familyName": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"possibleAuthenticators": [
|
||||
"AUTHENTICATOR_TYPE_USERNAME",
|
||||
"AUTHENTICATOR_TYPE_PASSWORD"
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
This will return something similar to:
|
||||
```json
|
||||
{
|
||||
"id": "257199613398745508",
|
||||
"details": {
|
||||
"sequence": "2",
|
||||
"change_date": "2024-03-07T08:08:35.963956Z",
|
||||
"resource_owner": "253750309325636004"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
So you successfully create a schema and could use that to manage your users based on that.
|
||||
But let's first checkout some possibilities ZITADEL offers.
|
||||
|
||||
## Assign Permissions
|
||||
|
||||
In the first step we've created a very simple `user` schema with only `givenName` and `familyName`.
|
||||
This allows any user with the permission to edit the user's data to change these values.
|
||||
Let's now update the schema and add some more properties and restrict who's able to retrieve and change data.
|
||||
|
||||
By setting `urn:zitadel:schema:permission` to fields, we can define the permissions for that field of different user roles / context.
|
||||
|
||||
For example by adding it to the `givenName` and `familyName` we can keep the state from before, where any `owner` (e.g. ORG_OWNER)
|
||||
as well as the user themselves (`self`) are allowed to read (`r`) and write (`w`) the data.
|
||||
|
||||
Let's now assume our service provides some profile information of the user on a dedicated page.
|
||||
Since we do not want the user to be able to change that value, we set the permission of `self` to `r`, meaning they will be able
|
||||
to see the `profileUri` value, but cannot update it.
|
||||
|
||||
Maybe we also have some `customerId`, which the user should not even know about. We therefore can simply omit the `self` permission
|
||||
and only set `owner` to `rw`, so admins are able to read and change the id if needed.
|
||||
|
||||
Finally, we call the [update user schema endpoint](/docs/apis/resources/user_schema_service_v3/user-schema-service-update-user-schema)
|
||||
with the following data. Be sure to provide the id of the previously created schema.
|
||||
|
||||
```bash
|
||||
curl -X PUT "https://$CUSTOM-DOMAIN/v3alpha/user_schemas/$SCHEMA_ID" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
--data-raw '{
|
||||
"schema": {
|
||||
"$schema": "urn:zitadel:schema:v1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"givenName": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"owner": "rw",
|
||||
"self": "rw"
|
||||
}
|
||||
},
|
||||
"familyName": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"owner": "rw",
|
||||
"self": "rw"
|
||||
}
|
||||
},
|
||||
"profileUri": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"owner": "rw",
|
||||
"self": "r"
|
||||
}
|
||||
},
|
||||
"customerId": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"owner": "rw"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
@ -165,6 +165,7 @@ module.exports = {
|
||||
items: [
|
||||
"guides/manage/user/reg-create-user",
|
||||
"guides/manage/customize/user-metadata",
|
||||
"guides/manage/customize/user-schema",
|
||||
],
|
||||
},
|
||||
],
|
||||
|
1
go.mod
1
go.mod
@ -54,6 +54,7 @@ require (
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/rakyll/statik v0.1.7
|
||||
github.com/rs/cors v1.10.1
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||
github.com/sony/sonyflake v1.2.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
|
2
go.sum
2
go.sum
@ -694,6 +694,8 @@ github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgY
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
150
internal/api/grpc/user/schema/v3alpha/schema.go
Normal file
150
internal/api/grpc/user/schema/v3alpha/schema.go
Normal 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
|
||||
}
|
||||
}
|
812
internal/api/grpc/user/schema/v3alpha/schema_integration_test.go
Normal file
812
internal/api/grpc/user/schema/v3alpha/schema_integration_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
51
internal/api/grpc/user/schema/v3alpha/server.go
Normal file
51
internal/api/grpc/user/schema/v3alpha/server.go
Normal 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
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
180
internal/command/user_schema.go
Normal file
180
internal/command/user_schema.go
Normal 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
|
||||
}
|
112
internal/command/user_schema_model.go
Normal file
112
internal/command/user_schema_model.go
Normal 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
|
||||
}
|
912
internal/command/user_schema_test.go
Normal file
912
internal/command/user_schema_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
120
internal/domain/schema/permission.go
Normal file
120
internal/domain/schema/permission.go
Normal 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
|
||||
}
|
28
internal/domain/schema/permission.schema.v1.json
Normal file
28
internal/domain/schema/permission.schema.v1.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
253
internal/domain/schema/permission_test.go
Normal file
253
internal/domain/schema/permission_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
41
internal/domain/schema/schema.go
Normal file
41
internal/domain/schema/schema.go
Normal 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
|
||||
}
|
13
internal/domain/schema/zitadel.schema.v1.json
Normal file
13
internal/domain/schema/zitadel.schema.v1.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
26
internal/domain/user_schema.go
Normal file
26
internal/domain/user_schema.go
Normal 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
|
||||
)
|
@ -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"`
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -63,6 +63,10 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer {
|
||||
Event: feature_v2.SystemLegacyIntrospectionEventType,
|
||||
Reduce: reduceSystemSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.SystemUserSchemaEventType,
|
||||
Reduce: reduceSystemSetFeature[bool],
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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]{
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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]])
|
||||
}
|
||||
|
@ -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 (
|
||||
|
25
internal/repository/user/schema/aggregate.go
Normal file
25
internal/repository/user/schema/aggregate.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
11
internal/repository/user/schema/eventstore.go
Normal file
11
internal/repository/user/schema/eventstore.go
Normal 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])
|
||||
}
|
233
internal/repository/user/schema/schema.go
Normal file
233
internal/repository/user/schema/schema.go
Normal 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,
|
||||
}
|
||||
}
|
@ -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 версия не се поддържа
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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 не поддерживается
|
||||
|
@ -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:
|
||||
|
@ -23,13 +23,18 @@ message SetInstanceFeaturesRequest{
|
||||
description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature.";
|
||||
}
|
||||
];
|
||||
|
||||
optional bool oidc_legacy_introspection = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
|
||||
}
|
||||
];
|
||||
optional bool user_schema = 4 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetInstanceFeaturesResponse {
|
||||
@ -73,4 +78,11 @@ message GetInstanceFeaturesResponse {
|
||||
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
|
||||
}
|
||||
];
|
||||
|
||||
FeatureFlag user_schema = 5 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
@ -31,6 +31,13 @@ message SetSystemFeaturesRequest{
|
||||
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
|
||||
}
|
||||
];
|
||||
|
||||
optional bool user_schema = 4 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetSystemFeaturesResponse {
|
||||
@ -67,4 +74,11 @@ message GetSystemFeaturesResponse {
|
||||
description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature.";
|
||||
}
|
||||
];
|
||||
|
||||
FeatureFlag user_schema = 5 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
@ -365,7 +365,7 @@ message CreateUserSchemaRequest {
|
||||
}
|
||||
// Defines the possible types of authenticators.
|
||||
repeated AuthenticatorType possible_authenticators = 3 [
|
||||
(validate.rules).repeated = {unique: true, items: {enum: {defined_only: true}}},
|
||||
(validate.rules).repeated = {unique: true, items: {enum: {defined_only: true, not_in: [0]}}},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "[\"AUTHENTICATOR_TYPE_USERNAME\",\"AUTHENTICATOR_TYPE_PASSWORD\",\"AUTHENTICATOR_TYPE_WEBAUTHN\"]";
|
||||
}
|
||||
@ -407,7 +407,7 @@ message UpdateUserSchemaRequest {
|
||||
//
|
||||
// Removal of an authenticator does not remove the authenticator on a user.
|
||||
repeated AuthenticatorType possible_authenticators = 4 [
|
||||
(validate.rules).repeated = {unique: true, items: {enum: {defined_only: true}}},
|
||||
(validate.rules).repeated = {unique: true, items: {enum: {defined_only: true, not_in: [0]}}},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "[\"AUTHENTICATOR_TYPE_USERNAME\",\"AUTHENTICATOR_TYPE_PASSWORD\",\"AUTHENTICATOR_TYPE_WEBAUTHN\"]";
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user