mirror of
https://github.com/zitadel/zitadel.git
synced 2025-06-02 22:28:19 +00:00
feat(api): feature flags (#7356)
* feat(api): feature API proto definitions * update proto based on discussion with @livio-a * cleanup old feature flag stuff * authz instance queries * align defaults * projection definitions * define commands and event reducers * implement system and instance setter APIs * api getter implementation * unit test repository package * command unit tests * unit test Get queries * grpc converter unit tests * migrate the V1 features * migrate oidc to dynamic features * projection unit test * fix instance by host * fix instance by id data type in sql * fix linting errors * add system projection test * fix behavior inversion * resolve proto file comments * rename SystemDefaultLoginInstanceEventType to SystemLoginDefaultOrgEventType so it's consistent with the instance level event * use write models and conditional set events * system features integration tests * instance features integration tests * error on empty request * documentation entry * typo in feature.proto * fix start unit tests * solve linting error on key case switch * remove system defaults after discussion with @eliobischof * fix system feature projection * resolve comments in defaults.yaml --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
parent
2801167668
commit
26d1563643
@ -334,11 +334,6 @@ OIDC:
|
||||
Path: /oauth/v2/device_authorization # ZITADEL_OIDC_CUSTOMENDPOINTS_DEVICEAUTH_PATH
|
||||
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2
|
||||
DefaultLogoutURLV2: "/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2
|
||||
Features:
|
||||
# Wheter projection triggers are used in the new Introspection implementation.
|
||||
TriggerIntrospectionProjections: false
|
||||
# Allows fallback to the Legacy Introspection implementation
|
||||
LegacyIntrospection: false
|
||||
PublicKeyCacheMaxAge: 24h # ZITADEL_OIDC_PUBLICKEYCACHEMAXAGE
|
||||
|
||||
SAML:
|
||||
@ -431,7 +426,6 @@ SystemAPIUsers:
|
||||
# Configure the SystemAPIUsers by environment variable using JSON notation:
|
||||
# ZITADEL_SYSTEMAPIUSERS='{"systemuser":{"Path":"/path/to/superuser/key.pem"},"systemuser2":{"KeyData":"<base64 encoded key>"}}'
|
||||
|
||||
#TODO: remove as soon as possible
|
||||
SystemDefaults:
|
||||
SecretGenerators:
|
||||
PasswordSaltCost: 14 # ZITADEL_SYSTEMDEFAULTS_SECRETGENERATORS_PASSWORDSALTCOST
|
||||
@ -833,8 +827,13 @@ 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:
|
||||
- FeatureLoginDefaultOrg: true
|
||||
LoginDefaultOrg: true # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINDEFAULTORG
|
||||
# TriggerIntrospectionProjections: false # ZITADEL_DEFAULTINSTANCE_FEATURES_TRIGGERINTROSPECTIONPROJECTIONS
|
||||
# LegacyIntrospection: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LEGACYINTROSPECTION
|
||||
Limits:
|
||||
# AuditLogRetention limits the number of events that can be queried via the events API by their age.
|
||||
# A value of "0s" means that all events are available.
|
||||
@ -910,7 +909,9 @@ InternalAuthZ:
|
||||
- "system.debug.read"
|
||||
- "system.debug.write"
|
||||
- "system.debug.delete"
|
||||
- "system.feature.read"
|
||||
- "system.feature.write"
|
||||
- "system.feature.delete"
|
||||
- "system.limits.write"
|
||||
- "system.limits.delete"
|
||||
- "system.quota.write"
|
||||
@ -921,6 +922,7 @@ InternalAuthZ:
|
||||
- "system.instance.read"
|
||||
- "system.domain.read"
|
||||
- "system.debug.read"
|
||||
- "system.feature.read"
|
||||
- "system.iam.member.read"
|
||||
- Role: "IAM_OWNER"
|
||||
Permissions:
|
||||
@ -941,7 +943,9 @@ InternalAuthZ:
|
||||
- "iam.flow.read"
|
||||
- "iam.flow.write"
|
||||
- "iam.flow.delete"
|
||||
- "iam.feature.read"
|
||||
- "iam.feature.write"
|
||||
- "iam.feature.delete"
|
||||
- "iam.restrictions.read"
|
||||
- "iam.restrictions.write"
|
||||
- "org.read"
|
||||
@ -961,6 +965,9 @@ InternalAuthZ:
|
||||
- "org.flow.read"
|
||||
- "org.flow.write"
|
||||
- "org.flow.delete"
|
||||
- "org.feature.read"
|
||||
- "org.feature.write"
|
||||
- "org.feature.delete"
|
||||
- "user.read"
|
||||
- "user.global.read"
|
||||
- "user.write"
|
||||
@ -971,6 +978,9 @@ InternalAuthZ:
|
||||
- "user.membership.read"
|
||||
- "user.credential.write"
|
||||
- "user.passkey.write"
|
||||
- "user.feature.read"
|
||||
- "user.feature.write"
|
||||
- "user.feature.delete"
|
||||
- "policy.read"
|
||||
- "policy.write"
|
||||
- "policy.delete"
|
||||
@ -1010,15 +1020,18 @@ InternalAuthZ:
|
||||
- "iam.action.read"
|
||||
- "iam.flow.read"
|
||||
- "iam.restrictions.read"
|
||||
- "iam.feature.read"
|
||||
- "org.read"
|
||||
- "org.member.read"
|
||||
- "org.idp.read"
|
||||
- "org.action.read"
|
||||
- "org.flow.read"
|
||||
- "org.feature.read"
|
||||
- "user.read"
|
||||
- "user.global.read"
|
||||
- "user.grant.read"
|
||||
- "user.membership.read"
|
||||
- "user.feature.read"
|
||||
- "policy.read"
|
||||
- "project.read"
|
||||
- "project.member.read"
|
||||
@ -1047,6 +1060,9 @@ InternalAuthZ:
|
||||
- "org.flow.read"
|
||||
- "org.flow.write"
|
||||
- "org.flow.delete"
|
||||
- "org.feature.read"
|
||||
- "org.feature.write"
|
||||
- "org.feature.delete"
|
||||
- "user.read"
|
||||
- "user.global.read"
|
||||
- "user.write"
|
||||
@ -1057,6 +1073,9 @@ InternalAuthZ:
|
||||
- "user.membership.read"
|
||||
- "user.credential.write"
|
||||
- "user.passkey.write"
|
||||
- "user.feature.read"
|
||||
- "user.feature.write"
|
||||
- "user.feature.delete"
|
||||
- "policy.read"
|
||||
- "policy.write"
|
||||
- "policy.delete"
|
||||
@ -1095,6 +1114,9 @@ InternalAuthZ:
|
||||
- "user.grant.delete"
|
||||
- "user.membership.read"
|
||||
- "user.passkey.write"
|
||||
- "user.feature.read"
|
||||
- "user.feature.write"
|
||||
- "user.feature.delete"
|
||||
- "project.read"
|
||||
- "project.member.read"
|
||||
- "project.role.read"
|
||||
@ -1122,6 +1144,9 @@ InternalAuthZ:
|
||||
- "org.flow.read"
|
||||
- "org.flow.write"
|
||||
- "org.flow.delete"
|
||||
- "org.feature.read"
|
||||
- "org.feature.write"
|
||||
- "org.feature.delete"
|
||||
- "user.read"
|
||||
- "user.global.read"
|
||||
- "user.write"
|
||||
@ -1132,6 +1157,9 @@ InternalAuthZ:
|
||||
- "user.membership.read"
|
||||
- "user.credential.write"
|
||||
- "user.passkey.write"
|
||||
- "user.feature.read"
|
||||
- "user.feature.write"
|
||||
- "user.feature.delete"
|
||||
- "policy.read"
|
||||
- "policy.write"
|
||||
- "policy.delete"
|
||||
@ -1165,6 +1193,9 @@ InternalAuthZ:
|
||||
- "user.grant.write"
|
||||
- "user.grant.delete"
|
||||
- "user.membership.read"
|
||||
- "user.feature.read"
|
||||
- "user.feature.write"
|
||||
- "user.feature.delete"
|
||||
- "policy.read"
|
||||
- "project.read"
|
||||
- "project.role.read"
|
||||
@ -1176,10 +1207,12 @@ InternalAuthZ:
|
||||
- "org.idp.read"
|
||||
- "org.action.read"
|
||||
- "org.flow.read"
|
||||
- "org.feature.read"
|
||||
- "user.read"
|
||||
- "user.global.read"
|
||||
- "user.grant.read"
|
||||
- "user.membership.read"
|
||||
- "user.feature.read"
|
||||
- "policy.read"
|
||||
- "project.read"
|
||||
- "project.member.read"
|
||||
@ -1196,6 +1229,9 @@ InternalAuthZ:
|
||||
- "org.idp.read"
|
||||
- "org.idp.write"
|
||||
- "org.idp.delete"
|
||||
- "org.feature.read"
|
||||
- "org.feature.write"
|
||||
- "org.feature.delete"
|
||||
- "policy.read"
|
||||
- "policy.write"
|
||||
- "policy.delete"
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/config/hook"
|
||||
"github.com/zitadel/zitadel/internal/config/network"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@ -27,7 +26,6 @@ func MustNewConfig(v *viper.Viper) *Config {
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
hook.EnumHookFunc(domain.FeatureString),
|
||||
hook.EnumHookFunc(internal_authz.MemberTypeString),
|
||||
)),
|
||||
)
|
||||
|
@ -24,7 +24,7 @@ type FirstInstance struct {
|
||||
Org command.InstanceOrgSetup
|
||||
MachineKeyPath string
|
||||
PatPath string
|
||||
Features map[domain.Feature]any
|
||||
Features *command.InstanceFeatures
|
||||
|
||||
instanceSetup command.InstanceSetup
|
||||
userEncryptionKey *crypto.KeyConfig
|
||||
|
@ -19,7 +19,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/config/hook"
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/notification/handlers"
|
||||
@ -67,7 +66,6 @@ func MustNewConfig(v *viper.Viper) *Config {
|
||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
database.DecodeHook,
|
||||
hook.EnumHookFunc(domain.FeatureString),
|
||||
hook.EnumHookFunc(authz.MemberTypeString),
|
||||
actions.HTTPConfigDecodeHook,
|
||||
hooks.MapTypeStringDecode[string, *authz.SystemAPIUser],
|
||||
@ -127,7 +125,6 @@ func MustNewSteps(v *viper.Viper) *Steps {
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
hook.EnumHookFunc(domain.FeatureString),
|
||||
)),
|
||||
)
|
||||
logging.OnError(err).Fatal("unable to read steps")
|
||||
|
@ -97,7 +97,6 @@ func MustNewConfig(v *viper.Viper) *Config {
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
database.DecodeHook,
|
||||
actions.HTTPConfigDecodeHook,
|
||||
hook.EnumHookFunc(domain.FeatureString),
|
||||
hook.EnumHookFunc(internal_authz.MemberTypeString),
|
||||
)),
|
||||
)
|
||||
|
@ -8,12 +8,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
@ -70,7 +72,9 @@ Log:
|
||||
args: args{yaml: `
|
||||
DefaultInstance:
|
||||
Features:
|
||||
- FeatureLoginDefaultOrg: true
|
||||
LoginDefaultOrg: true
|
||||
LegacyIntrospection: true
|
||||
TriggerIntrospectionProjections: true
|
||||
Log:
|
||||
Level: info
|
||||
Actions:
|
||||
@ -78,25 +82,10 @@ Actions:
|
||||
DenyList: []
|
||||
`},
|
||||
want: func(t *testing.T, config *Config) {
|
||||
assert.Equal(t, config.DefaultInstance.Features, map[domain.Feature]any{
|
||||
domain.FeatureLoginDefaultOrg: true,
|
||||
})
|
||||
},
|
||||
}, {
|
||||
name: "features string ok",
|
||||
args: args{yaml: `
|
||||
DefaultInstance:
|
||||
Features: >
|
||||
[{"featureLoginDefaultOrg": true}]
|
||||
Log:
|
||||
Level: info
|
||||
Actions:
|
||||
HTTP:
|
||||
DenyList: []
|
||||
`},
|
||||
want: func(t *testing.T, config *Config) {
|
||||
assert.Equal(t, config.DefaultInstance.Features, map[domain.Feature]any{
|
||||
domain.FeatureLoginDefaultOrg: true,
|
||||
assert.Equal(t, config.DefaultInstance.Features, &command.InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(true),
|
||||
})
|
||||
},
|
||||
}, {
|
||||
|
@ -29,7 +29,6 @@ import (
|
||||
"github.com/zitadel/zitadel/cmd/encryption"
|
||||
"github.com/zitadel/zitadel/cmd/key"
|
||||
cmd_tls "github.com/zitadel/zitadel/cmd/tls"
|
||||
"github.com/zitadel/zitadel/feature"
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
||||
"github.com/zitadel/zitadel/internal/api"
|
||||
@ -38,6 +37,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/admin"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/auth"
|
||||
execution_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/execution/v3alpha"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/feature/v2"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/management"
|
||||
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/org/v2"
|
||||
@ -404,6 +404,9 @@ func startAPIs(
|
||||
if err := apis.RegisterService(ctx, org.CreateServer(commands, queries, permissionCheck)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, feature.CreateServer(commands, queries)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, execution_v3_alpha.CreateServer(commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -469,7 +472,6 @@ func startAPIs(
|
||||
keys.User,
|
||||
keys.IDPConfig,
|
||||
keys.CSRFCookieKey,
|
||||
feature.NewCheck(eventstore),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start login: %w", err)
|
||||
|
@ -311,6 +311,13 @@ module.exports = {
|
||||
groupPathsBy: "tag",
|
||||
},
|
||||
},
|
||||
feature_v2: {
|
||||
specPath: ".artifacts/openapi/zitadel/feature/v2beta/feature_service.swagger.json",
|
||||
outputDir: "docs/apis/resources/feature_service_v2",
|
||||
sidebarOptions: {
|
||||
groupPathsBy: "tag",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -694,6 +694,20 @@ module.exports = {
|
||||
},
|
||||
items: require("./docs/apis/resources/settings_service/sidebar.js"),
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Feature Lifecycle (Beta)",
|
||||
link: {
|
||||
type: "generated-index",
|
||||
title: "Feature Service API (Beta)",
|
||||
slug: "/apis/resources/feature_service",
|
||||
description:
|
||||
"This API is intended to manage features for ZITADEL. Feature settings that are available on multiple \"levels\", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op.\n" +
|
||||
"\n" +
|
||||
"This project is in beta state. It can AND will continue breaking until a stable version is released.",
|
||||
},
|
||||
items: require("./docs/apis/resources/feature_service_v2/sidebar.js"),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -1,45 +0,0 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
)
|
||||
|
||||
type Checker interface {
|
||||
CheckInstanceBooleanFeature(ctx context.Context, f domain.Feature) (feature.Boolean, error)
|
||||
}
|
||||
|
||||
func NewCheck(eventstore *eventstore.Eventstore) *Check {
|
||||
return &Check{eventstore: eventstore}
|
||||
}
|
||||
|
||||
type Check struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
|
||||
func (c *Check) CheckInstanceBooleanFeature(ctx context.Context, f domain.Feature) (feature.Boolean, error) {
|
||||
return getInstanceFeature[feature.Boolean](ctx, f, c.eventstore.Filter)
|
||||
}
|
||||
|
||||
func getInstanceFeature[T feature.SetEventType](ctx context.Context, f domain.Feature, filter preparation.FilterToQueryReducer) (T, error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
writeModel, err := command.NewInstanceFeatureWriteModel[T](instanceID, f)
|
||||
if err != nil {
|
||||
return writeModel.Value, err
|
||||
}
|
||||
events, err := filter(ctx, writeModel.Query())
|
||||
if err != nil {
|
||||
return writeModel.Value, err
|
||||
}
|
||||
writeModel.AppendEvents(events...)
|
||||
if err = writeModel.Reduce(); err != nil {
|
||||
return writeModel.Value, err
|
||||
}
|
||||
return writeModel.Value, nil
|
||||
}
|
@ -5,6 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -23,6 +25,7 @@ type Instance interface {
|
||||
SecurityPolicyAllowedOrigins() []string
|
||||
Block() *bool
|
||||
AuditLogRetention() *time.Duration
|
||||
Features() feature.Features
|
||||
}
|
||||
|
||||
type InstanceVerifier interface {
|
||||
@ -37,6 +40,7 @@ type instance struct {
|
||||
appID string
|
||||
clientID string
|
||||
orgID string
|
||||
features feature.Features
|
||||
}
|
||||
|
||||
func (i *instance) Block() *bool {
|
||||
@ -83,6 +87,10 @@ func (i *instance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *instance) Features() feature.Features {
|
||||
return i.features
|
||||
}
|
||||
|
||||
func GetInstance(ctx context.Context) Instance {
|
||||
instance, ok := ctx.Value(instanceKey).(Instance)
|
||||
if !ok {
|
||||
@ -91,6 +99,10 @@ func GetInstance(ctx context.Context) Instance {
|
||||
return instance
|
||||
}
|
||||
|
||||
func GetFeatures(ctx context.Context) feature.Features {
|
||||
return GetInstance(ctx).Features()
|
||||
}
|
||||
|
||||
func WithInstance(ctx context.Context, instance Instance) context.Context {
|
||||
return context.WithValue(ctx, instanceKey, instance)
|
||||
}
|
||||
@ -120,3 +132,12 @@ func WithConsole(ctx context.Context, projectID, appID string) context.Context {
|
||||
//i.clientID = clientID
|
||||
return context.WithValue(ctx, instanceKey, i)
|
||||
}
|
||||
|
||||
func WithFeatures(ctx context.Context, f feature.Features) context.Context {
|
||||
i, ok := ctx.Value(instanceKey).(*instance)
|
||||
if !ok {
|
||||
i = new(instance)
|
||||
}
|
||||
i.features = f
|
||||
return context.WithValue(ctx, instanceKey, i)
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
)
|
||||
|
||||
func Test_Instance(t *testing.T) {
|
||||
@ -17,6 +19,7 @@ func Test_Instance(t *testing.T) {
|
||||
instanceID string
|
||||
projectID string
|
||||
consoleID string
|
||||
features feature.Features
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -56,6 +59,19 @@ func Test_Instance(t *testing.T) {
|
||||
consoleID: "consoleID",
|
||||
},
|
||||
},
|
||||
{
|
||||
"WithFeatures",
|
||||
args{
|
||||
WithFeatures(context.Background(), feature.Features{
|
||||
LoginDefaultOrg: true,
|
||||
}),
|
||||
},
|
||||
res{
|
||||
features: feature.Features{
|
||||
LoginDefaultOrg: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -63,6 +79,7 @@ func Test_Instance(t *testing.T) {
|
||||
assert.Equal(t, tt.res.instanceID, got.InstanceID())
|
||||
assert.Equal(t, tt.res.projectID, got.ProjectID())
|
||||
assert.Equal(t, tt.res.consoleID, got.ConsoleClientID())
|
||||
assert.Equal(t, tt.res.features, got.Features())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -112,3 +129,7 @@ func (m *mockInstance) RequestedHost() string {
|
||||
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInstance) Features() feature.Features {
|
||||
return feature.Features{}
|
||||
}
|
||||
|
@ -3,13 +3,17 @@ package admin
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
object_pb "github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
)
|
||||
|
||||
func (s *Server) ActivateFeatureLoginDefaultOrg(ctx context.Context, _ *admin_pb.ActivateFeatureLoginDefaultOrgRequest) (*admin_pb.ActivateFeatureLoginDefaultOrgResponse, error) {
|
||||
details, err := s.command.SetBooleanInstanceFeature(ctx, domain.FeatureLoginDefaultOrg, true)
|
||||
details, err := s.command.SetInstanceFeatures(ctx, &command.InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
71
internal/api/grpc/feature/v2/converter.go
Normal file
71
internal/api/grpc/feature/v2/converter.go
Normal file
@ -0,0 +1,71 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
)
|
||||
|
||||
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures {
|
||||
return &command.SystemFeatures{
|
||||
LoginDefaultOrg: req.LoginDefaultOrg,
|
||||
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
|
||||
LegacyIntrospection: req.OidcLegacyIntrospection,
|
||||
}
|
||||
}
|
||||
|
||||
func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesResponse {
|
||||
return &feature_pb.GetSystemFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(f.Details),
|
||||
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
|
||||
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
|
||||
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
|
||||
}
|
||||
}
|
||||
|
||||
func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *command.InstanceFeatures {
|
||||
return &command.InstanceFeatures{
|
||||
LoginDefaultOrg: req.LoginDefaultOrg,
|
||||
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
|
||||
LegacyIntrospection: req.OidcLegacyIntrospection,
|
||||
}
|
||||
}
|
||||
|
||||
func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeaturesResponse {
|
||||
return &feature_pb.GetInstanceFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(f.Details),
|
||||
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
|
||||
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
|
||||
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
|
||||
}
|
||||
}
|
||||
|
||||
func featureSourceToFlagPb(fs *query.FeatureSource[bool]) *feature_pb.FeatureFlag {
|
||||
return &feature_pb.FeatureFlag{
|
||||
Enabled: fs.Value,
|
||||
Source: featureLevelToSourcePb(fs.Level),
|
||||
}
|
||||
}
|
||||
|
||||
func featureLevelToSourcePb(level feature.Level) feature_pb.Source {
|
||||
switch level {
|
||||
case feature.LevelUnspecified:
|
||||
return feature_pb.Source_SOURCE_UNSPECIFIED
|
||||
case feature.LevelSystem:
|
||||
return feature_pb.Source_SOURCE_SYSTEM
|
||||
case feature.LevelInstance:
|
||||
return feature_pb.Source_SOURCE_INSTANCE
|
||||
case feature.LevelOrg:
|
||||
return feature_pb.Source_SOURCE_ORGANIZATION
|
||||
case feature.LevelProject:
|
||||
return feature_pb.Source_SOURCE_PROJECT
|
||||
case feature.LevelApp:
|
||||
return feature_pb.Source_SOURCE_APP
|
||||
case feature.LevelUser:
|
||||
return feature_pb.Source_SOURCE_USER
|
||||
default:
|
||||
return feature_pb.Source(level)
|
||||
}
|
||||
}
|
188
internal/api/grpc/feature/v2/converter_test.go
Normal file
188
internal/api/grpc/feature/v2/converter_test.go
Normal file
@ -0,0 +1,188 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
)
|
||||
|
||||
func Test_systemFeaturesToCommand(t *testing.T) {
|
||||
arg := &feature_pb.SetSystemFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(false),
|
||||
OidcLegacyIntrospection: nil,
|
||||
}
|
||||
want := &command.SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: nil,
|
||||
}
|
||||
got := systemFeaturesToCommand(arg)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func Test_systemFeaturesToPb(t *testing.T) {
|
||||
arg := &query.SystemFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(123, 0),
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
LoginDefaultOrg: query.FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
TriggerIntrospectionProjections: query.FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
LegacyIntrospection: query.FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
}
|
||||
want := &feature_pb.GetSystemFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
ChangeDate: ×tamppb.Timestamp{Seconds: 123},
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
LoginDefaultOrg: &feature_pb.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature_pb.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcLegacyIntrospection: &feature_pb.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
}
|
||||
got := systemFeaturesToPb(arg)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func Test_instanceFeaturesToCommand(t *testing.T) {
|
||||
arg := &feature_pb.SetInstanceFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(false),
|
||||
OidcLegacyIntrospection: nil,
|
||||
}
|
||||
want := &command.InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: nil,
|
||||
}
|
||||
got := instanceFeaturesToCommand(arg)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func Test_instanceFeaturesToPb(t *testing.T) {
|
||||
arg := &query.InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(123, 0),
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: query.FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
TriggerIntrospectionProjections: query.FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
LegacyIntrospection: query.FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
}
|
||||
want := &feature_pb.GetInstanceFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
ChangeDate: ×tamppb.Timestamp{Seconds: 123},
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: &feature_pb.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature_pb.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcLegacyIntrospection: &feature_pb.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
}
|
||||
got := instanceFeaturesToPb(arg)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func Test_featureLevelToSourcePb(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
level feature.Level
|
||||
want feature_pb.Source
|
||||
}{
|
||||
{
|
||||
name: "unspecified",
|
||||
level: feature.LevelUnspecified,
|
||||
want: feature_pb.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
name: "system",
|
||||
level: feature.LevelSystem,
|
||||
want: feature_pb.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
{
|
||||
name: "instance",
|
||||
level: feature.LevelInstance,
|
||||
want: feature_pb.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
{
|
||||
name: "org",
|
||||
level: feature.LevelOrg,
|
||||
want: feature_pb.Source_SOURCE_ORGANIZATION,
|
||||
},
|
||||
{
|
||||
name: "project",
|
||||
level: feature.LevelProject,
|
||||
want: feature_pb.Source_SOURCE_PROJECT,
|
||||
},
|
||||
{
|
||||
name: "app",
|
||||
level: feature.LevelApp,
|
||||
want: feature_pb.Source_SOURCE_APP,
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
level: feature.LevelUser,
|
||||
want: feature_pb.Source_SOURCE_USER,
|
||||
},
|
||||
{
|
||||
name: "unknown",
|
||||
level: 99,
|
||||
want: 99,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := featureLevelToSourcePb(tt.level)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
86
internal/api/grpc/feature/v2/feature.go
Normal file
86
internal/api/grpc/feature/v2/feature.go
Normal file
@ -0,0 +1,86 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) {
|
||||
details, err := s.command.SetSystemFeatures(ctx, systemFeaturesToCommand(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.SetSystemFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ResetSystemFeatures(ctx context.Context, req *feature.ResetSystemFeaturesRequest) (_ *feature.ResetSystemFeaturesResponse, err error) {
|
||||
details, err := s.command.ResetSystemFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.ResetSystemFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetSystemFeatures(ctx context.Context, req *feature.GetSystemFeaturesRequest) (_ *feature.GetSystemFeaturesResponse, err error) {
|
||||
f, err := s.query.GetSystemFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return systemFeaturesToPb(f), nil
|
||||
}
|
||||
|
||||
func (s *Server) SetInstanceFeatures(ctx context.Context, req *feature.SetInstanceFeaturesRequest) (_ *feature.SetInstanceFeaturesResponse, err error) {
|
||||
details, err := s.command.SetInstanceFeatures(ctx, instanceFeaturesToCommand(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.SetInstanceFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ResetInstanceFeatures(ctx context.Context, req *feature.ResetInstanceFeaturesRequest) (_ *feature.ResetInstanceFeaturesResponse, err error) {
|
||||
details, err := s.command.ResetInstanceFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.ResetInstanceFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetInstanceFeatures(ctx context.Context, req *feature.GetInstanceFeaturesRequest) (_ *feature.GetInstanceFeaturesResponse, err error) {
|
||||
f, err := s.query.GetInstanceFeatures(ctx, req.GetInheritance())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return instanceFeaturesToPb(f), nil
|
||||
}
|
||||
|
||||
func (s *Server) SetOrganizationFeatures(ctx context.Context, req *feature.SetOrganizationFeaturesRequest) (_ *feature.SetOrganizationFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetOrganizationFeatures not implemented")
|
||||
}
|
||||
func (s *Server) ResetOrganizationFeatures(ctx context.Context, req *feature.ResetOrganizationFeaturesRequest) (_ *feature.ResetOrganizationFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ResetOrganizationFeatures not implemented")
|
||||
}
|
||||
func (s *Server) GetOrganizationFeatures(ctx context.Context, req *feature.GetOrganizationFeaturesRequest) (_ *feature.GetOrganizationFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetOrganizationFeatures not implemented")
|
||||
}
|
||||
func (s *Server) SetUserFeatures(ctx context.Context, req *feature.SetUserFeatureRequest) (_ *feature.SetUserFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetUserFeatures not implemented")
|
||||
}
|
||||
func (s *Server) ResetUserFeatures(ctx context.Context, req *feature.ResetUserFeaturesRequest) (_ *feature.ResetUserFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ResetUserFeatures not implemented")
|
||||
}
|
||||
func (s *Server) GetUserFeatures(ctx context.Context, req *feature.GetUserFeaturesRequest) (_ *feature.GetUserFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserFeatures not implemented")
|
||||
}
|
470
internal/api/grpc/feature/v2/feature_integration_test.go
Normal file
470
internal/api/grpc/feature/v2/feature_integration_test.go
Normal file
@ -0,0 +1,470 @@
|
||||
//go:build integration
|
||||
|
||||
package feature_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
SystemCTX context.Context
|
||||
IamCTX context.Context
|
||||
OrgCTX context.Context
|
||||
Tester *integration.Tester
|
||||
Client feature.FeatureServiceClient
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, _, cancel := integration.Contexts(5 * time.Minute)
|
||||
defer cancel()
|
||||
Tester = integration.NewTester(ctx)
|
||||
SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
|
||||
IamCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
|
||||
OrgCTX = Tester.WithAuthorization(ctx, integration.OrgOwner)
|
||||
|
||||
defer Tester.Done()
|
||||
Client = Tester.Client.FeatureV2
|
||||
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func TestServer_SetSystemFeatures(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.SetSystemFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *feature.SetSystemFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.SetSystemFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no changes error",
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.SetSystemFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.SetSystemFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
want: &feature.SetSystemFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
got, err := Client.SetSystemFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ResetSystemFeatures(t *testing.T) {
|
||||
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
want *feature.ResetSystemFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
ctx: IamCTX,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
ctx: SystemCTX,
|
||||
want: &feature.ResetSystemFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.ResetSystemFeatures(tt.ctx, &feature.ResetSystemFeaturesRequest{})
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_GetSystemFeatures(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.GetSystemFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare func(t *testing.T)
|
||||
args args
|
||||
want *feature.GetSystemFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetSystemFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nothing set",
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.GetSystemFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetSystemFeaturesResponse{},
|
||||
},
|
||||
{
|
||||
name: "some features",
|
||||
prepare: func(t *testing.T) {
|
||||
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(false),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.GetSystemFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetSystemFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
if tt.prepare != nil {
|
||||
tt.prepare(t)
|
||||
}
|
||||
got, err := Client.GetSystemFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
|
||||
assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
|
||||
assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_SetInstanceFeatures(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.SetInstanceFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *feature.SetInstanceFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: OrgCTX,
|
||||
req: &feature.SetInstanceFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no changes error",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.SetInstanceFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.SetInstanceFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
want: &feature.SetInstanceFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
got, err := Client.SetInstanceFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ResetInstanceFeatures(t *testing.T) {
|
||||
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
want *feature.ResetInstanceFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
ctx: OrgCTX,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
ctx: IamCTX,
|
||||
want: &feature.ResetInstanceFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.ResetInstanceFeatures(tt.ctx, &feature.ResetInstanceFeaturesRequest{})
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_GetInstanceFeatures(t *testing.T) {
|
||||
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
|
||||
OidcLegacyIntrospection: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.GetInstanceFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare func(t *testing.T)
|
||||
args args
|
||||
want *feature.GetInstanceFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: OrgCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "defaults, no inheritance",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{},
|
||||
},
|
||||
{
|
||||
name: "defaults, inheritance",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{
|
||||
Inheritance: true,
|
||||
},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcLegacyIntrospection: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "some features, no inheritance",
|
||||
prepare: func(t *testing.T) {
|
||||
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(false),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "one feature, inheritance",
|
||||
prepare: func(t *testing.T) {
|
||||
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{
|
||||
Inheritance: true,
|
||||
},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcLegacyIntrospection: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
if tt.prepare != nil {
|
||||
tt.prepare(t)
|
||||
}
|
||||
got, err := Client.GetInstanceFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
|
||||
assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
|
||||
assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertFeatureFlag(t *testing.T, expected, actual *feature.FeatureFlag) {
|
||||
t.Helper()
|
||||
assert.Equal(t, expected.GetEnabled(), actual.GetEnabled(), "enabled")
|
||||
assert.Equal(t, expected.GetSource(), actual.GetSource(), "source")
|
||||
}
|
47
internal/api/grpc/feature/v2/server.go
Normal file
47
internal/api/grpc/feature/v2/server.go
Normal file
@ -0,0 +1,47 @@
|
||||
package feature
|
||||
|
||||
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"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
feature.UnimplementedFeatureServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
|
||||
feature.RegisterFeatureServiceServer(grpcServer, s)
|
||||
}
|
||||
|
||||
func (s *Server) AppName() string {
|
||||
return feature.FeatureService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return feature.FeatureService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
return feature.FeatureService_AuthMethods
|
||||
}
|
||||
|
||||
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
|
||||
return feature.RegisterFeatureServiceHandler
|
||||
}
|
@ -22,9 +22,9 @@ func InstanceToPb(instance *query.Instance) *instance_pb.Instance {
|
||||
instance.Sequence,
|
||||
instance.CreationDate,
|
||||
instance.ChangeDate,
|
||||
instance.InstanceID(),
|
||||
instance.ID,
|
||||
),
|
||||
Id: instance.InstanceID(),
|
||||
Id: instance.ID,
|
||||
Name: instance.Name,
|
||||
Domains: DomainsToPb(instance.Domains),
|
||||
Version: build.Version(),
|
||||
@ -38,9 +38,9 @@ func InstanceDetailToPb(instance *query.Instance) *instance_pb.InstanceDetail {
|
||||
instance.Sequence,
|
||||
instance.CreationDate,
|
||||
instance.ChangeDate,
|
||||
instance.InstanceID(),
|
||||
instance.ID,
|
||||
),
|
||||
Id: instance.InstanceID(),
|
||||
Id: instance.ID,
|
||||
Name: instance.Name,
|
||||
Domains: DomainsToPb(instance.Domains),
|
||||
Version: build.Version(),
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
)
|
||||
|
||||
func Test_hostNameFromContext(t *testing.T) {
|
||||
@ -208,3 +209,7 @@ func (m *mockInstance) RequestedHost() string {
|
||||
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInstance) Features() feature.Features {
|
||||
return feature.Features{}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
object_pb "github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
system_pb "github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
@ -22,12 +23,14 @@ func (s *Server) SetInstanceFeature(ctx context.Context, req *system_pb.SetInsta
|
||||
|
||||
func (s *Server) setInstanceFeature(ctx context.Context, req *system_pb.SetInstanceFeatureRequest) (*domain.ObjectDetails, error) {
|
||||
feat := domain.Feature(req.FeatureId)
|
||||
if !feat.IsAFeature() {
|
||||
if feat != domain.FeatureLoginDefaultOrg {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "SYST-SGV45", "Errors.Feature.NotExisting")
|
||||
}
|
||||
switch t := req.Value.(type) {
|
||||
case *system_pb.SetInstanceFeatureRequest_Bool:
|
||||
return s.command.SetBooleanInstanceFeature(ctx, feat, t.Bool)
|
||||
return s.command.SetInstanceFeatures(ctx, &command.InstanceFeatures{
|
||||
LoginDefaultOrg: &t.Bool,
|
||||
})
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "SYST-dag5g", "Errors.Feature.TypeNotSupported")
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ func (s *Server) ListDomains(ctx context.Context, req *system_pb.ListDomainsRequ
|
||||
}
|
||||
|
||||
func (s *Server) AddDomain(ctx context.Context, req *system_pb.AddDomainRequest) (*system_pb.AddDomainResponse, error) {
|
||||
instance, err := s.query.Instance(ctx, true)
|
||||
instance, err := s.query.InstanceByID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
zitadel_http "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
)
|
||||
|
||||
func Test_instanceInterceptor_Handler(t *testing.T) {
|
||||
@ -343,3 +344,7 @@ func (m *mockInstance) RequestedHost() string {
|
||||
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInstance) Features() feature.Features {
|
||||
return feature.Features{}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/user/model"
|
||||
@ -95,7 +96,7 @@ func (s *Server) assertClientScopesForPAT(ctx context.Context, token *accessToke
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal")
|
||||
}
|
||||
roles, err := s.query.SearchProjectRoles(ctx, s.features.TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
|
||||
roles, err := s.query.SearchProjectRoles(ctx, authz.GetFeatures(ctx).TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
@ -23,10 +24,11 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
|
||||
span.EndWithError(err)
|
||||
}()
|
||||
|
||||
if s.features.LegacyIntrospection {
|
||||
features := authz.GetFeatures(ctx)
|
||||
if features.LegacyIntrospection {
|
||||
return s.LegacyServer.Introspect(ctx, r)
|
||||
}
|
||||
if s.features.TriggerIntrospectionProjections {
|
||||
if features.TriggerIntrospectionProjections {
|
||||
// Execute all triggers in one concurrent sweep.
|
||||
query.TriggerIntrospectionProjections(ctx)
|
||||
}
|
||||
|
@ -42,7 +42,6 @@ type Config struct {
|
||||
DeviceAuth *DeviceAuthorizationConfig
|
||||
DefaultLoginURLV2 string
|
||||
DefaultLogoutURLV2 string
|
||||
Features Features
|
||||
PublicKeyCacheMaxAge time.Duration
|
||||
}
|
||||
|
||||
@ -62,11 +61,6 @@ type Endpoint struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type Features struct {
|
||||
TriggerIntrospectionProjections bool
|
||||
LegacyIntrospection bool
|
||||
}
|
||||
|
||||
type OPStorage struct {
|
||||
repo repository.Repository
|
||||
command *command.Commands
|
||||
@ -128,7 +122,6 @@ func NewServer(
|
||||
|
||||
server := &Server{
|
||||
LegacyServer: op.NewLegacyServer(provider, endpoints(config.CustomEndpoints)),
|
||||
features: config.Features,
|
||||
repo: repo,
|
||||
query: query,
|
||||
command: command,
|
||||
|
@ -21,7 +21,6 @@ import (
|
||||
type Server struct {
|
||||
http.Handler
|
||||
*op.LegacyServer
|
||||
features Features
|
||||
|
||||
repo repository.Repository
|
||||
query *query.Queries
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/zitadel/zitadel/feature"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
@ -39,7 +38,6 @@ type Login struct {
|
||||
samlAuthCallbackURL func(context.Context, string) string
|
||||
idpConfigAlg crypto.EncryptionAlgorithm
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
featureCheck feature.Checker
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@ -76,7 +74,6 @@ func CreateLogin(config Config,
|
||||
userCodeAlg crypto.EncryptionAlgorithm,
|
||||
idpConfigAlg crypto.EncryptionAlgorithm,
|
||||
csrfCookieKey []byte,
|
||||
featureCheck feature.Checker,
|
||||
) (*Login, error) {
|
||||
login := &Login{
|
||||
oidcAuthCallbackURL: oidcAuthCallbackURL,
|
||||
@ -89,7 +86,6 @@ func CreateLogin(config Config,
|
||||
authRepo: authRepo,
|
||||
idpConfigAlg: idpConfigAlg,
|
||||
userCodeAlg: userCodeAlg,
|
||||
featureCheck: featureCheck,
|
||||
}
|
||||
csrfInterceptor := createCSRFInterceptor(config.CSRFCookieName, csrfCookieKey, externalSecure, login.csrfErrorHandler())
|
||||
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
|
||||
|
@ -524,12 +524,12 @@ func (l *Login) getOrgID(r *http.Request, authReq *domain.AuthRequest) string {
|
||||
}
|
||||
|
||||
func (l *Login) getPrivateLabelingID(r *http.Request, authReq *domain.AuthRequest) string {
|
||||
defaultID := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
f, err := l.featureCheck.CheckInstanceBooleanFeature(r.Context(), domain.FeatureLoginDefaultOrg)
|
||||
logging.OnError(err).Warnf("could not check feature %s", domain.FeatureLoginDefaultOrg)
|
||||
if !f.Boolean {
|
||||
defaultID = authz.GetInstance(r.Context()).InstanceID()
|
||||
instance := authz.GetInstance(r.Context())
|
||||
defaultID := instance.DefaultOrganisationID()
|
||||
if !instance.Features().LoginDefaultOrg {
|
||||
defaultID = instance.InstanceID()
|
||||
}
|
||||
|
||||
if authReq != nil {
|
||||
return authReq.PrivateLabelingOrgID(defaultID)
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/feature"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
|
||||
cache "github.com/zitadel/zitadel/internal/auth_request/repository"
|
||||
@ -50,8 +49,6 @@ type AuthRequestRepo struct {
|
||||
ApplicationProvider applicationProvider
|
||||
CustomTextProvider customTextProvider
|
||||
|
||||
FeatureCheck feature.Checker
|
||||
|
||||
IdGenerator id.Generator
|
||||
}
|
||||
|
||||
@ -656,16 +653,15 @@ func (repo *AuthRequestRepo) getLoginPolicyAndIDPProviders(ctx context.Context,
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.AuthRequest) error {
|
||||
instance := authz.GetInstance(ctx)
|
||||
orgID := request.RequestedOrgID
|
||||
if orgID == "" {
|
||||
orgID = request.UserOrgID
|
||||
}
|
||||
if orgID == "" {
|
||||
orgID = authz.GetInstance(ctx).DefaultOrganisationID()
|
||||
f, err := repo.FeatureCheck.CheckInstanceBooleanFeature(ctx, domain.FeatureLoginDefaultOrg)
|
||||
logging.WithFields("authReq", request.ID).OnError(err).Warnf("could not check feature %s", domain.FeatureLoginDefaultOrg)
|
||||
if !f.Boolean {
|
||||
orgID = authz.GetInstance(ctx).InstanceID()
|
||||
if !instance.Features().LoginDefaultOrg {
|
||||
orgID = instance.InstanceID()
|
||||
}
|
||||
}
|
||||
|
||||
@ -692,7 +688,7 @@ func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.A
|
||||
return err
|
||||
}
|
||||
request.LabelPolicy = labelPolicy
|
||||
defaultLoginTranslations, err := repo.getLoginTexts(ctx, authz.GetInstance(ctx).InstanceID())
|
||||
defaultLoginTranslations, err := repo.getLoginTexts(ctx, instance.InstanceID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package eventsourcing
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/feature"
|
||||
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/eventstore"
|
||||
auth_handler "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/handler"
|
||||
auth_view "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
|
||||
@ -77,7 +76,6 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, c
|
||||
ProjectProvider: queryView,
|
||||
ApplicationProvider: queries,
|
||||
CustomTextProvider: queries,
|
||||
FeatureCheck: feature.NewCheck(esV2),
|
||||
IdGenerator: id.SonyFlakeGenerator(),
|
||||
},
|
||||
eventstore.TokenRepo{
|
||||
|
@ -15,7 +15,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/limits"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
@ -110,7 +109,7 @@ type InstanceSetup struct {
|
||||
SMTPConfiguration *smtp.Config
|
||||
OIDCSettings *OIDCSettings
|
||||
Quotas *SetQuotas
|
||||
Features map[domain.Feature]any
|
||||
Features *InstanceFeatures
|
||||
Limits *SetLimits
|
||||
Restrictions *SetRestrictions
|
||||
}
|
||||
@ -313,9 +312,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
||||
setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain)
|
||||
setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg)
|
||||
setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg)
|
||||
if err := setupFeatures(c, &validations, setup.Features, instanceID); err != nil {
|
||||
return "", "", nil, nil, err
|
||||
}
|
||||
setupFeatures(&validations, setup.Features, instanceID)
|
||||
setupLimits(c, &validations, limitsAgg, setup.Limits)
|
||||
setupRestrictions(c, &validations, restrictionsAgg, setup.Restrictions)
|
||||
|
||||
@ -368,20 +365,8 @@ func setupQuotas(commands *Commands, validations *[]preparation.Validation, setQ
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupFeatures(commands *Commands, validations *[]preparation.Validation, enableFeatures map[domain.Feature]any, instanceID string) error {
|
||||
for f, value := range enableFeatures {
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
wm, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*validations = append(*validations, prepareSetFeature(wm, feature.Boolean{Boolean: v}, commands.idGenerator))
|
||||
default:
|
||||
return zerrors.ThrowInvalidArgument(nil, "INST-GE4tg", "Errors.Feature.TypeNotSupported")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
func setupFeatures(validations *[]preparation.Validation, features *InstanceFeatures, instanceID string) {
|
||||
*validations = append(*validations, prepareSetFeatures(instanceID, features))
|
||||
}
|
||||
|
||||
func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation, oidcSettings *OIDCSettings, instanceAgg *instance.Aggregate) {
|
||||
|
@ -1,63 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func (c *Commands) SetBooleanInstanceFeature(ctx context.Context, f domain.Feature, value bool) (*domain.ObjectDetails, error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
writeModel, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter,
|
||||
prepareSetFeature(writeModel, feature.Boolean{Boolean: value}, c.idGenerator))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cmds) == 0 {
|
||||
return writeModelToObjectDetails(&writeModel.FeatureWriteModel.WriteModel), nil
|
||||
}
|
||||
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(pushedEvents), nil
|
||||
}
|
||||
|
||||
func prepareSetFeature[T feature.SetEventType](writeModel *InstanceFeatureWriteModel[T], value T, idGenerator id.Generator) preparation.Validation {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
if !writeModel.feature.IsAFeature() || writeModel.feature == domain.FeatureUnspecified {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "FEAT-JK3td", "Errors.Feature.NotExisting")
|
||||
}
|
||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
events, err := filter(ctx, writeModel.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writeModel.AppendEvents(events...)
|
||||
if err = writeModel.Reduce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(events) == 0 {
|
||||
writeModel.AggregateID, err = idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
setEvent, err := writeModel.Set(ctx, value)
|
||||
if err != nil || setEvent == nil {
|
||||
return nil, err
|
||||
}
|
||||
return []eventstore.Command{setEvent}, nil
|
||||
}, nil
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type FeatureWriteModel[T feature.SetEventType] struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
feature domain.Feature
|
||||
|
||||
Value T
|
||||
}
|
||||
|
||||
func NewFeatureWriteModel[T feature.SetEventType](instanceID, resourceOwner string, feature domain.Feature) (*FeatureWriteModel[T], error) {
|
||||
wm := &FeatureWriteModel[T]{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
InstanceID: instanceID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
feature: feature,
|
||||
}
|
||||
if wm.Value.FeatureType() != feature.Type() {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "FEAT-AS4k1", "Errors.Feature.InvalidValue")
|
||||
}
|
||||
return wm, nil
|
||||
}
|
||||
func (wm *FeatureWriteModel[T]) Set(ctx context.Context, value T) (event *feature.SetEvent[T], err error) {
|
||||
if wm.Value == value {
|
||||
return nil, nil
|
||||
}
|
||||
return feature.NewSetEvent[T](
|
||||
ctx,
|
||||
&feature.NewAggregate(wm.AggregateID, wm.ResourceOwner).Aggregate,
|
||||
wm.eventType(),
|
||||
value,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (wm *FeatureWriteModel[T]) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(feature.AggregateType).
|
||||
EventTypes(wm.eventType()).
|
||||
Builder()
|
||||
}
|
||||
|
||||
func (wm *FeatureWriteModel[T]) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature.SetEvent[T]:
|
||||
wm.Value = e.Value
|
||||
default:
|
||||
return zerrors.ThrowPreconditionFailed(nil, "FEAT-SDfjk", "Errors.Feature.TypeNotSupported")
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *FeatureWriteModel[T]) eventType() eventstore.EventType {
|
||||
return feature.EventTypeFromFeature(wm.feature)
|
||||
}
|
||||
|
||||
type InstanceFeatureWriteModel[T feature.SetEventType] struct {
|
||||
FeatureWriteModel[T]
|
||||
}
|
||||
|
||||
func NewInstanceFeatureWriteModel[T feature.SetEventType](instanceID string, feature domain.Feature) (*InstanceFeatureWriteModel[T], error) {
|
||||
wm, err := NewFeatureWriteModel[T](instanceID, instanceID, feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &InstanceFeatureWriteModel[T]{
|
||||
FeatureWriteModel: *wm,
|
||||
}, nil
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"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/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_SetBooleanInstanceFeature(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
f domain.Feature
|
||||
value bool
|
||||
}
|
||||
type res struct {
|
||||
details *domain.ObjectDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"unknown feature",
|
||||
fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureUnspecified,
|
||||
value: true,
|
||||
},
|
||||
res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "FEAT-AS4k1", "Errors.Feature.InvalidValue"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"wrong type",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
// as there's currently no other [feature.SetEventType] than [feature.Boolean],
|
||||
// we need to use a completely other event type to demonstrate the behaviour
|
||||
instance.NewInstanceAddedEvent(context.Background(), &instance.NewAggregate("instanceID").Aggregate,
|
||||
"instance",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: true,
|
||||
},
|
||||
res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "FEAT-SDfjk", "Errors.Feature.TypeNotSupported"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"first set",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: true},
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "featureID"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: true,
|
||||
},
|
||||
res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instanceID",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"update flag",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: true},
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: false},
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: false,
|
||||
},
|
||||
res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instanceID",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"no change",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: true},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: true,
|
||||
},
|
||||
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),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
got, err := c.SetBooleanInstanceFeature(tt.args.ctx, tt.args.f, tt.args.value)
|
||||
assert.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.details, got)
|
||||
})
|
||||
}
|
||||
}
|
69
internal/command/instance_features.go
Normal file
69
internal/command/instance_features.go
Normal file
@ -0,0 +1,69 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type InstanceFeatures struct {
|
||||
LoginDefaultOrg *bool
|
||||
TriggerIntrospectionProjections *bool
|
||||
LegacyIntrospection *bool
|
||||
}
|
||||
|
||||
func (m *InstanceFeatures) isEmpty() bool {
|
||||
return m.LoginDefaultOrg == nil &&
|
||||
m.TriggerIntrospectionProjections == nil &&
|
||||
m.LegacyIntrospection == nil
|
||||
}
|
||||
|
||||
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {
|
||||
if f.isEmpty() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Vigh1", "Errors.NoChangesFound")
|
||||
}
|
||||
wm := NewInstanceFeaturesWriteModel(authz.GetInstance(ctx).InstanceID())
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds := wm.setCommands(ctx, f)
|
||||
if len(cmds) == 0 {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
events, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
||||
|
||||
func prepareSetFeatures(instanceID string, f *InstanceFeatures) preparation.Validation {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
wm := NewInstanceFeaturesWriteModel(instanceID)
|
||||
return wm.setCommands(ctx, f), nil
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commands) ResetInstanceFeatures(ctx context.Context) (*domain.ObjectDetails, error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
wm := NewInstanceFeaturesWriteModel(instanceID)
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wm.isEmpty() {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
aggregate := feature_v2.NewAggregate(instanceID, instanceID)
|
||||
events, err := c.eventstore.Push(ctx, feature_v2.NewResetEvent(ctx, aggregate, feature_v2.InstanceResetEventType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
93
internal/command/instance_features_model.go
Normal file
93
internal/command/instance_features_model.go
Normal file
@ -0,0 +1,93 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
type InstanceFeaturesWriteModel struct {
|
||||
*eventstore.WriteModel
|
||||
InstanceFeatures
|
||||
}
|
||||
|
||||
func NewInstanceFeaturesWriteModel(instanceID string) *InstanceFeaturesWriteModel {
|
||||
m := &InstanceFeaturesWriteModel{
|
||||
WriteModel: &eventstore.WriteModel{
|
||||
AggregateID: instanceID,
|
||||
ResourceOwner: instanceID,
|
||||
},
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) Reduce() (err error) {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature_v2.ResetEvent:
|
||||
m.reduceReset()
|
||||
case *feature_v1.SetEvent[feature_v1.Boolean]:
|
||||
err = m.reduceBoolFeature(
|
||||
feature_v1.DefaultLoginInstanceEventToV2(e),
|
||||
)
|
||||
case *feature_v2.SetEvent[bool]:
|
||||
err = m.reduceBoolFeature(e)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return m.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AddQuery().
|
||||
AggregateTypes(feature_v2.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
EventTypes(
|
||||
feature_v1.DefaultLoginInstanceEventType,
|
||||
feature_v2.InstanceResetEventType,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) reduceReset() {
|
||||
m.LoginDefaultOrg = nil
|
||||
m.TriggerIntrospectionProjections = nil
|
||||
m.LegacyIntrospection = nil
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
|
||||
_, key, err := event.FeatureInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch key {
|
||||
case feature.KeyUnspecified:
|
||||
return nil
|
||||
case feature.KeyLoginDefaultOrg:
|
||||
m.LoginDefaultOrg = &event.Value
|
||||
case feature.KeyTriggerIntrospectionProjections:
|
||||
m.TriggerIntrospectionProjections = &event.Value
|
||||
case feature.KeyLegacyIntrospection:
|
||||
m.LegacyIntrospection = &event.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *InstanceFeatures) []eventstore.Command {
|
||||
aggregate := feature_v2.NewAggregate(wm.AggregateID, wm.ResourceOwner)
|
||||
cmds := make([]eventstore.Command, 0, len(feature.KeyValues())-1)
|
||||
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)
|
||||
return cmds
|
||||
}
|
323
internal/command/instance_features_test.go
Normal file
323
internal/command/instance_features_test.go
Normal file
@ -0,0 +1,323 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_SetInstanceFeatures(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
aggregate := feature_v2.NewAggregate("instance1", "instance1")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
f *InstanceFeatures
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "all nil, No Change",
|
||||
eventstore: expectEventstore(),
|
||||
args: args{ctx, &InstanceFeatures{}},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-Vigh1", "Errors.NoChangesFound"),
|
||||
},
|
||||
{
|
||||
name: "set LoginDefaultOrg",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set LoginDefaultOrg, update from v1",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v1.NewSetEvent[feature_v1.Boolean](
|
||||
ctx, &eventstore.Aggregate{
|
||||
ID: "instance1",
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
feature_v1.DefaultLoginInstanceEventType,
|
||||
feature_v1.Boolean{
|
||||
Boolean: false,
|
||||
},
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set TriggerIntrospectionProjections",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
TriggerIntrospectionProjections: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set LegacyIntrospection",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "set all",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set only updated",
|
||||
eventstore: expectEventstore(
|
||||
// throw in some set events, reset and set again.
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.SetInstanceFeatures(tt.args.ctx, tt.args.f)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_ResetInstanceFeatures(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
aggregate := feature_v2.NewAggregate("instance1", "instance1")
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewResetEvent(ctx, aggregate, feature_v2.InstanceResetEventType),
|
||||
),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewResetEvent(ctx, aggregate, feature_v2.InstanceResetEventType),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change after previous reset",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceResetEventType,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change without previous events",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.ResetInstanceFeatures(ctx)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
@ -215,6 +216,10 @@ func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInstance) Features() feature.Features {
|
||||
return feature.Features{}
|
||||
}
|
||||
|
||||
func newMockPermissionCheckAllowed() domain.PermissionCheck {
|
||||
return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||
return nil
|
||||
|
56
internal/command/system_features.go
Normal file
56
internal/command/system_features.go
Normal file
@ -0,0 +1,56 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type SystemFeatures struct {
|
||||
LoginDefaultOrg *bool
|
||||
TriggerIntrospectionProjections *bool
|
||||
LegacyIntrospection *bool
|
||||
}
|
||||
|
||||
func (m *SystemFeatures) isEmpty() bool {
|
||||
return m.LoginDefaultOrg == nil &&
|
||||
m.TriggerIntrospectionProjections == nil &&
|
||||
m.LegacyIntrospection == nil
|
||||
}
|
||||
|
||||
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {
|
||||
if f.isEmpty() {
|
||||
return nil, zerrors.ThrowInternal(nil, "COMMAND-Oop8a", "Errors.NoChangesFound")
|
||||
}
|
||||
wm := NewSystemFeaturesWriteModel()
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds := wm.setCommands(ctx, f)
|
||||
if len(cmds) == 0 {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
events, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
||||
|
||||
func (c *Commands) ResetSystemFeatures(ctx context.Context) (*domain.ObjectDetails, error) {
|
||||
wm := NewSystemFeaturesWriteModel()
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wm.isEmpty() {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
aggregate := feature_v2.NewAggregate("SYSTEM", "SYSTEM")
|
||||
events, err := c.eventstore.Push(ctx, feature_v2.NewResetEvent(ctx, aggregate, feature_v2.SystemResetEventType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
94
internal/command/system_features_model.go
Normal file
94
internal/command/system_features_model.go
Normal file
@ -0,0 +1,94 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
type SystemFeaturesWriteModel struct {
|
||||
*eventstore.WriteModel
|
||||
SystemFeatures
|
||||
}
|
||||
|
||||
func NewSystemFeaturesWriteModel() *SystemFeaturesWriteModel {
|
||||
m := &SystemFeaturesWriteModel{
|
||||
WriteModel: &eventstore.WriteModel{
|
||||
AggregateID: "SYSTEM",
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) Reduce() (err error) {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature_v2.ResetEvent:
|
||||
m.reduceReset()
|
||||
case *feature_v2.SetEvent[bool]:
|
||||
err = m.reduceBoolFeature(e)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return m.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AddQuery().
|
||||
AggregateTypes(feature_v2.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
EventTypes(
|
||||
feature_v2.SystemResetEventType,
|
||||
feature_v2.SystemLoginDefaultOrgEventType,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType,
|
||||
feature_v2.SystemLegacyIntrospectionEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) reduceReset() {
|
||||
m.LoginDefaultOrg = nil
|
||||
m.TriggerIntrospectionProjections = nil
|
||||
m.LegacyIntrospection = nil
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
|
||||
_, key, err := event.FeatureInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch key {
|
||||
case feature.KeyUnspecified:
|
||||
return nil
|
||||
case feature.KeyLoginDefaultOrg:
|
||||
m.LoginDefaultOrg = &event.Value
|
||||
case feature.KeyTriggerIntrospectionProjections:
|
||||
m.TriggerIntrospectionProjections = &event.Value
|
||||
case feature.KeyLegacyIntrospection:
|
||||
m.LegacyIntrospection = &event.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFeatures) []eventstore.Command {
|
||||
aggregate := feature_v2.NewAggregate(wm.AggregateID, wm.ResourceOwner)
|
||||
cmds := make([]eventstore.Command, 0, len(feature.KeyValues())-1)
|
||||
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)
|
||||
return cmds
|
||||
}
|
||||
|
||||
func appendFeatureUpdate[T comparable](ctx context.Context, cmds []eventstore.Command, aggregate *feature_v2.Aggregate, oldValue, newValue *T, eventType eventstore.EventType) []eventstore.Command {
|
||||
if newValue != nil && (oldValue == nil || *oldValue != *newValue) {
|
||||
cmds = append(cmds, feature_v2.NewSetEvent[T](ctx, aggregate, eventType, *newValue))
|
||||
}
|
||||
return cmds
|
||||
}
|
290
internal/command/system_features_test.go
Normal file
290
internal/command/system_features_test.go
Normal file
@ -0,0 +1,290 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_SetSystemFeatures(t *testing.T) {
|
||||
aggregate := feature_v2.NewAggregate("SYSTEM", "SYSTEM")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
f *SystemFeatures
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "all nil, No Change",
|
||||
eventstore: expectEventstore(),
|
||||
args: args{context.Background(), &SystemFeatures{}},
|
||||
wantErr: zerrors.ThrowInternal(nil, "COMMAND-Oop8a", "Errors.NoChangesFound"),
|
||||
},
|
||||
{
|
||||
name: "set LoginDefaultOrg",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set TriggerIntrospectionProjections",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
TriggerIntrospectionProjections: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set LegacyIntrospection",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "set all",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set only updated",
|
||||
eventstore: expectEventstore(
|
||||
// throw in some set events, reset and set again.
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.SetSystemFeatures(tt.args.ctx, tt.args.f)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_ResetSystemFeatures(t *testing.T) {
|
||||
aggregate := feature_v2.NewAggregate("SYSTEM", "SYSTEM")
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewResetEvent(context.Background(), aggregate, feature_v2.SystemResetEventType),
|
||||
),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewResetEvent(context.Background(), aggregate, feature_v2.SystemResetEventType),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change after previous reset",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemResetEventType,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change without previous events",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.ResetSystemFeatures(context.Background())
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
// Code generated by "enumer -type Feature"; DO NOT EDIT.
|
||||
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _FeatureName = "FeatureUnspecifiedFeatureLoginDefaultOrg"
|
||||
|
||||
var _FeatureIndex = [...]uint8{0, 18, 40}
|
||||
|
||||
const _FeatureLowerName = "featureunspecifiedfeaturelogindefaultorg"
|
||||
|
||||
func (i Feature) String() string {
|
||||
if i < 0 || i >= Feature(len(_FeatureIndex)-1) {
|
||||
return fmt.Sprintf("Feature(%d)", i)
|
||||
}
|
||||
return _FeatureName[_FeatureIndex[i]:_FeatureIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _FeatureNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[FeatureUnspecified-(0)]
|
||||
_ = x[FeatureLoginDefaultOrg-(1)]
|
||||
}
|
||||
|
||||
var _FeatureValues = []Feature{FeatureUnspecified, FeatureLoginDefaultOrg}
|
||||
|
||||
var _FeatureNameToValueMap = map[string]Feature{
|
||||
_FeatureName[0:18]: FeatureUnspecified,
|
||||
_FeatureLowerName[0:18]: FeatureUnspecified,
|
||||
_FeatureName[18:40]: FeatureLoginDefaultOrg,
|
||||
_FeatureLowerName[18:40]: FeatureLoginDefaultOrg,
|
||||
}
|
||||
|
||||
var _FeatureNames = []string{
|
||||
_FeatureName[0:18],
|
||||
_FeatureName[18:40],
|
||||
}
|
||||
|
||||
// FeatureString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func FeatureString(s string) (Feature, error) {
|
||||
if val, ok := _FeatureNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _FeatureNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to Feature values", s)
|
||||
}
|
||||
|
||||
// FeatureValues returns all values of the enum
|
||||
func FeatureValues() []Feature {
|
||||
return _FeatureValues
|
||||
}
|
||||
|
||||
// FeatureStrings returns a slice of all String values of the enum
|
||||
func FeatureStrings() []string {
|
||||
strs := make([]string, len(_FeatureNames))
|
||||
copy(strs, _FeatureNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAFeature returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i Feature) IsAFeature() bool {
|
||||
for _, v := range _FeatureValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ObjectDetails struct {
|
||||
Sequence uint64
|
||||
|
@ -532,6 +532,12 @@ func NewIsNullCond(column string) Condition {
|
||||
}
|
||||
}
|
||||
|
||||
func NewIsNotNullCond(column string) Condition {
|
||||
return func(string) (string, []any) {
|
||||
return column + " IS NOT NULL", nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewTextArrayContainsCond returns a Condition that checks if the column that stores an array of text contains the given value
|
||||
func NewTextArrayContainsCond(column string, value string) Condition {
|
||||
return func(param string) (string, []any) {
|
||||
|
@ -44,7 +44,6 @@ func (rm *ReadModel) Reduce() error {
|
||||
rm.ChangeDate = rm.Events[len(rm.Events)-1].CreatedAt()
|
||||
rm.ProcessedSequence = rm.Events[len(rm.Events)-1].Sequence()
|
||||
// all events processed and not needed anymore
|
||||
rm.Events = nil
|
||||
rm.Events = []Event{}
|
||||
rm.Events = rm.Events[0:0]
|
||||
return nil
|
||||
}
|
||||
|
30
internal/feature/feature.go
Normal file
30
internal/feature/feature.go
Normal file
@ -0,0 +1,30 @@
|
||||
package feature
|
||||
|
||||
//go:generate enumer -type Key -transform snake -trimprefix Key
|
||||
type Key int
|
||||
|
||||
const (
|
||||
KeyUnspecified Key = iota
|
||||
KeyLoginDefaultOrg
|
||||
KeyTriggerIntrospectionProjections
|
||||
KeyLegacyIntrospection
|
||||
)
|
||||
|
||||
//go:generate enumer -type Level -transform snake -trimprefix Level
|
||||
type Level int
|
||||
|
||||
const (
|
||||
LevelUnspecified Level = iota
|
||||
LevelSystem
|
||||
LevelInstance
|
||||
LevelOrg
|
||||
LevelProject
|
||||
LevelApp
|
||||
LevelUser
|
||||
)
|
||||
|
||||
type Features struct {
|
||||
LoginDefaultOrg bool `json:"login_default_org,omitempty"`
|
||||
TriggerIntrospectionProjections bool `json:"trigger_introspection_projections,omitempty"`
|
||||
LegacyIntrospection bool `json:"legacy_introspection,omitempty"`
|
||||
}
|
43
internal/feature/feature_test.go
Normal file
43
internal/feature/feature_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestKey(t *testing.T) {
|
||||
tests := []string{
|
||||
"unspecified",
|
||||
"login_default_org",
|
||||
"trigger_introspection_projections",
|
||||
"legacy_introspection",
|
||||
}
|
||||
for _, want := range tests {
|
||||
t.Run(want, func(t *testing.T) {
|
||||
feature, err := KeyString(want)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, feature.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevel(t *testing.T) {
|
||||
tests := []string{
|
||||
"unspecified",
|
||||
"system",
|
||||
"instance",
|
||||
"org",
|
||||
"project",
|
||||
"app",
|
||||
"user",
|
||||
}
|
||||
for _, want := range tests {
|
||||
t.Run(want, func(t *testing.T) {
|
||||
level, err := LevelString(want)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, level.String())
|
||||
})
|
||||
}
|
||||
}
|
86
internal/feature/key_enumer.go
Normal file
86
internal/feature/key_enumer.go
Normal file
@ -0,0 +1,86 @@
|
||||
// Code generated by "enumer -type Key -transform snake -trimprefix Key"; DO NOT EDIT.
|
||||
|
||||
package feature
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspection"
|
||||
|
||||
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81}
|
||||
|
||||
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspection"
|
||||
|
||||
func (i Key) String() string {
|
||||
if i < 0 || i >= Key(len(_KeyIndex)-1) {
|
||||
return fmt.Sprintf("Key(%d)", i)
|
||||
}
|
||||
return _KeyName[_KeyIndex[i]:_KeyIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _KeyNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[KeyUnspecified-(0)]
|
||||
_ = x[KeyLoginDefaultOrg-(1)]
|
||||
_ = x[KeyTriggerIntrospectionProjections-(2)]
|
||||
_ = x[KeyLegacyIntrospection-(3)]
|
||||
}
|
||||
|
||||
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection}
|
||||
|
||||
var _KeyNameToValueMap = map[string]Key{
|
||||
_KeyName[0:11]: KeyUnspecified,
|
||||
_KeyLowerName[0:11]: KeyUnspecified,
|
||||
_KeyName[11:28]: KeyLoginDefaultOrg,
|
||||
_KeyLowerName[11:28]: KeyLoginDefaultOrg,
|
||||
_KeyName[28:61]: KeyTriggerIntrospectionProjections,
|
||||
_KeyLowerName[28:61]: KeyTriggerIntrospectionProjections,
|
||||
_KeyName[61:81]: KeyLegacyIntrospection,
|
||||
_KeyLowerName[61:81]: KeyLegacyIntrospection,
|
||||
}
|
||||
|
||||
var _KeyNames = []string{
|
||||
_KeyName[0:11],
|
||||
_KeyName[11:28],
|
||||
_KeyName[28:61],
|
||||
_KeyName[61:81],
|
||||
}
|
||||
|
||||
// KeyString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func KeyString(s string) (Key, error) {
|
||||
if val, ok := _KeyNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _KeyNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to Key values", s)
|
||||
}
|
||||
|
||||
// KeyValues returns all values of the enum
|
||||
func KeyValues() []Key {
|
||||
return _KeyValues
|
||||
}
|
||||
|
||||
// KeyStrings returns a slice of all String values of the enum
|
||||
func KeyStrings() []string {
|
||||
strs := make([]string, len(_KeyNames))
|
||||
copy(strs, _KeyNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAKey returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i Key) IsAKey() bool {
|
||||
for _, v := range _KeyValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
98
internal/feature/level_enumer.go
Normal file
98
internal/feature/level_enumer.go
Normal file
@ -0,0 +1,98 @@
|
||||
// Code generated by "enumer -type Level -transform snake -trimprefix Level"; DO NOT EDIT.
|
||||
|
||||
package feature
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _LevelName = "unspecifiedsysteminstanceorgprojectappuser"
|
||||
|
||||
var _LevelIndex = [...]uint8{0, 11, 17, 25, 28, 35, 38, 42}
|
||||
|
||||
const _LevelLowerName = "unspecifiedsysteminstanceorgprojectappuser"
|
||||
|
||||
func (i Level) String() string {
|
||||
if i < 0 || i >= Level(len(_LevelIndex)-1) {
|
||||
return fmt.Sprintf("Level(%d)", i)
|
||||
}
|
||||
return _LevelName[_LevelIndex[i]:_LevelIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _LevelNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[LevelUnspecified-(0)]
|
||||
_ = x[LevelSystem-(1)]
|
||||
_ = x[LevelInstance-(2)]
|
||||
_ = x[LevelOrg-(3)]
|
||||
_ = x[LevelProject-(4)]
|
||||
_ = x[LevelApp-(5)]
|
||||
_ = x[LevelUser-(6)]
|
||||
}
|
||||
|
||||
var _LevelValues = []Level{LevelUnspecified, LevelSystem, LevelInstance, LevelOrg, LevelProject, LevelApp, LevelUser}
|
||||
|
||||
var _LevelNameToValueMap = map[string]Level{
|
||||
_LevelName[0:11]: LevelUnspecified,
|
||||
_LevelLowerName[0:11]: LevelUnspecified,
|
||||
_LevelName[11:17]: LevelSystem,
|
||||
_LevelLowerName[11:17]: LevelSystem,
|
||||
_LevelName[17:25]: LevelInstance,
|
||||
_LevelLowerName[17:25]: LevelInstance,
|
||||
_LevelName[25:28]: LevelOrg,
|
||||
_LevelLowerName[25:28]: LevelOrg,
|
||||
_LevelName[28:35]: LevelProject,
|
||||
_LevelLowerName[28:35]: LevelProject,
|
||||
_LevelName[35:38]: LevelApp,
|
||||
_LevelLowerName[35:38]: LevelApp,
|
||||
_LevelName[38:42]: LevelUser,
|
||||
_LevelLowerName[38:42]: LevelUser,
|
||||
}
|
||||
|
||||
var _LevelNames = []string{
|
||||
_LevelName[0:11],
|
||||
_LevelName[11:17],
|
||||
_LevelName[17:25],
|
||||
_LevelName[25:28],
|
||||
_LevelName[28:35],
|
||||
_LevelName[35:38],
|
||||
_LevelName[38:42],
|
||||
}
|
||||
|
||||
// LevelString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func LevelString(s string) (Level, error) {
|
||||
if val, ok := _LevelNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _LevelNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to Level values", s)
|
||||
}
|
||||
|
||||
// LevelValues returns all values of the enum
|
||||
func LevelValues() []Level {
|
||||
return _LevelValues
|
||||
}
|
||||
|
||||
// LevelStrings returns a slice of all String values of the enum
|
||||
func LevelStrings() []string {
|
||||
strs := make([]string, len(_LevelNames))
|
||||
copy(strs, _LevelNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsALevel returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i Level) IsALevel() bool {
|
||||
for _, v := range _LevelValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -27,6 +27,7 @@ import (
|
||||
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/auth"
|
||||
execution "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
@ -49,6 +50,7 @@ type Client struct {
|
||||
OrgV2 organisation.OrganizationServiceClient
|
||||
System system.SystemServiceClient
|
||||
ExecutionV3 execution.ExecutionServiceClient
|
||||
FeatureV2 feature.FeatureServiceClient
|
||||
}
|
||||
|
||||
func newClient(cc *grpc.ClientConn) Client {
|
||||
@ -63,6 +65,7 @@ func newClient(cc *grpc.ClientConn) Client {
|
||||
OrgV2: organisation.NewOrganizationServiceClient(cc),
|
||||
System: system.NewSystemServiceClient(cc),
|
||||
ExecutionV3: execution.NewExecutionServiceClient(cc),
|
||||
FeatureV2: feature.NewFeatureServiceClient(cc),
|
||||
}
|
||||
}
|
||||
|
||||
|
14
internal/query/converter.go
Normal file
14
internal/query/converter.go
Normal file
@ -0,0 +1,14 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
func readModelToObjectDetails(model *eventstore.ReadModel) *domain.ObjectDetails {
|
||||
return &domain.ObjectDetails{
|
||||
Sequence: model.ProcessedSequence,
|
||||
ResourceOwner: model.ResourceOwner,
|
||||
EventDate: model.ChangeDate,
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ package query
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
@ -15,6 +17,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
@ -94,21 +97,12 @@ type Instance struct {
|
||||
Sequence uint64
|
||||
Name string
|
||||
|
||||
DefaultOrgID string
|
||||
IAMProjectID string
|
||||
ConsoleID string
|
||||
ConsoleAppID string
|
||||
DefaultLang language.Tag
|
||||
Domains []*InstanceDomain
|
||||
host string
|
||||
csp csp
|
||||
block *bool
|
||||
auditLogRetention *time.Duration
|
||||
}
|
||||
|
||||
type csp struct {
|
||||
enabled bool
|
||||
allowedOrigins database.TextArray[string]
|
||||
DefaultOrgID string
|
||||
IAMProjectID string
|
||||
ConsoleID string
|
||||
ConsoleAppID string
|
||||
DefaultLang language.Tag
|
||||
Domains []*InstanceDomain
|
||||
}
|
||||
|
||||
type Instances struct {
|
||||
@ -116,53 +110,6 @@ type Instances struct {
|
||||
Instances []*Instance
|
||||
}
|
||||
|
||||
func (i *Instance) InstanceID() string {
|
||||
return i.ID
|
||||
}
|
||||
|
||||
func (i *Instance) ProjectID() string {
|
||||
return i.IAMProjectID
|
||||
}
|
||||
|
||||
func (i *Instance) ConsoleClientID() string {
|
||||
return i.ConsoleID
|
||||
}
|
||||
|
||||
func (i *Instance) ConsoleApplicationID() string {
|
||||
return i.ConsoleAppID
|
||||
}
|
||||
|
||||
func (i *Instance) RequestedDomain() string {
|
||||
return strings.Split(i.host, ":")[0]
|
||||
}
|
||||
|
||||
func (i *Instance) RequestedHost() string {
|
||||
return i.host
|
||||
}
|
||||
|
||||
func (i *Instance) DefaultLanguage() language.Tag {
|
||||
return i.DefaultLang
|
||||
}
|
||||
|
||||
func (i *Instance) DefaultOrganisationID() string {
|
||||
return i.DefaultOrgID
|
||||
}
|
||||
|
||||
func (i *Instance) SecurityPolicyAllowedOrigins() []string {
|
||||
if !i.csp.enabled {
|
||||
return nil
|
||||
}
|
||||
return i.csp.allowedOrigins
|
||||
}
|
||||
|
||||
func (i *Instance) Block() *bool {
|
||||
return i.block
|
||||
}
|
||||
|
||||
func (i *Instance) AuditLogRetention() *time.Duration {
|
||||
return i.auditLogRetention
|
||||
}
|
||||
|
||||
type InstanceSearchQueries struct {
|
||||
SearchRequest
|
||||
Queries []SearchQuery
|
||||
@ -224,7 +171,7 @@ func (q *Queries) Instance(ctx context.Context, shouldTriggerBulk bool) (instanc
|
||||
traceSpan.EndWithError(err)
|
||||
}
|
||||
|
||||
stmt, scan := prepareInstanceDomainQuery(ctx, q.client, authz.GetInstance(ctx).RequestedDomain())
|
||||
stmt, scan := prepareInstanceDomainQuery(ctx, q.client)
|
||||
query, args, err := stmt.Where(sq.Eq{
|
||||
InstanceColumnID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
}).ToSql()
|
||||
@ -239,28 +186,34 @@ func (q *Queries) Instance(ctx context.Context, shouldTriggerBulk bool) (instanc
|
||||
return instance, err
|
||||
}
|
||||
|
||||
func (q *Queries) InstanceByHost(ctx context.Context, host string) (instance authz.Instance, err error) {
|
||||
var (
|
||||
//go:embed instance_by_domain.sql
|
||||
instanceByDomainQuery string
|
||||
|
||||
//go:embed instance_by_id.sql
|
||||
instanceByIDQuery string
|
||||
)
|
||||
|
||||
func (q *Queries) InstanceByHost(ctx context.Context, host string) (_ authz.Instance, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
stmt, scan := prepareAuthzInstanceQuery(ctx, q.client, host)
|
||||
host = strings.Split(host, ":")[0] //remove possible port
|
||||
query, args, err := stmt.Where(sq.Eq{
|
||||
InstanceDomainDomainCol.identifier(): host,
|
||||
}).ToSql()
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-SAfg2", "Errors.Query.SQLStatement")
|
||||
}
|
||||
|
||||
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
|
||||
instance, err = scan(rows)
|
||||
return err
|
||||
}, query, args...)
|
||||
domain := strings.Split(host, ":")[0] // remove possible port
|
||||
instance, scan := scanAuthzInstance(host, domain)
|
||||
err = q.client.QueryRowContext(ctx, scan, instanceByDomainQuery, domain)
|
||||
logging.OnError(err).WithField("host", host).WithField("domain", domain).Warn("instance by host")
|
||||
return instance, err
|
||||
}
|
||||
|
||||
func (q *Queries) InstanceByID(ctx context.Context) (_ authz.Instance, err error) {
|
||||
return q.Instance(ctx, true)
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
instance, scan := scanAuthzInstance("", "")
|
||||
err = q.client.QueryRowContext(ctx, scan, instanceByIDQuery, instanceID)
|
||||
logging.OnError(err).WithField("instance_id", instanceID).Warn("instance by ID")
|
||||
return instance, err
|
||||
}
|
||||
|
||||
func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag {
|
||||
@ -268,48 +221,7 @@ func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag {
|
||||
if err != nil {
|
||||
return language.Und
|
||||
}
|
||||
return instance.DefaultLanguage()
|
||||
}
|
||||
|
||||
func prepareInstanceQuery(ctx context.Context, db prepareDatabase, host string) (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) {
|
||||
return sq.Select(
|
||||
InstanceColumnID.identifier(),
|
||||
InstanceColumnCreationDate.identifier(),
|
||||
InstanceColumnChangeDate.identifier(),
|
||||
InstanceColumnSequence.identifier(),
|
||||
InstanceColumnDefaultOrgID.identifier(),
|
||||
InstanceColumnProjectID.identifier(),
|
||||
InstanceColumnConsoleID.identifier(),
|
||||
InstanceColumnConsoleAppID.identifier(),
|
||||
InstanceColumnDefaultLanguage.identifier(),
|
||||
).
|
||||
From(instanceTable.identifier() + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(row *sql.Row) (*Instance, error) {
|
||||
var (
|
||||
instance = &Instance{host: host}
|
||||
lang = ""
|
||||
)
|
||||
err := row.Scan(
|
||||
&instance.ID,
|
||||
&instance.CreationDate,
|
||||
&instance.ChangeDate,
|
||||
&instance.Sequence,
|
||||
&instance.DefaultOrgID,
|
||||
&instance.IAMProjectID,
|
||||
&instance.ConsoleID,
|
||||
&instance.ConsoleAppID,
|
||||
&lang,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, zerrors.ThrowNotFound(err, "QUERY-5m09s", "Errors.IAM.NotFound")
|
||||
}
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-3j9sf", "Errors.Internal")
|
||||
}
|
||||
instance.DefaultLang = language.Make(lang)
|
||||
return instance, nil
|
||||
}
|
||||
return instance.DefaultLang
|
||||
}
|
||||
|
||||
func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) {
|
||||
@ -417,7 +329,7 @@ func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu
|
||||
}
|
||||
}
|
||||
|
||||
func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase, host string) (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) {
|
||||
func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) {
|
||||
return sq.Select(
|
||||
InstanceColumnID.identifier(),
|
||||
InstanceColumnCreationDate.identifier(),
|
||||
@ -441,7 +353,6 @@ func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase, host st
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(rows *sql.Rows) (*Instance, error) {
|
||||
instance := &Instance{
|
||||
host: host,
|
||||
Domains: make([]*InstanceDomain, 0),
|
||||
}
|
||||
lang := ""
|
||||
@ -499,104 +410,123 @@ func prepareInstanceDomainQuery(ctx context.Context, db prepareDatabase, host st
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAuthzInstanceQuery(ctx context.Context, db prepareDatabase, host string) (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) {
|
||||
return sq.Select(
|
||||
InstanceColumnID.identifier(),
|
||||
InstanceColumnCreationDate.identifier(),
|
||||
InstanceColumnChangeDate.identifier(),
|
||||
InstanceColumnSequence.identifier(),
|
||||
InstanceColumnName.identifier(),
|
||||
InstanceColumnDefaultOrgID.identifier(),
|
||||
InstanceColumnProjectID.identifier(),
|
||||
InstanceColumnConsoleID.identifier(),
|
||||
InstanceColumnConsoleAppID.identifier(),
|
||||
InstanceColumnDefaultLanguage.identifier(),
|
||||
InstanceDomainDomainCol.identifier(),
|
||||
InstanceDomainIsPrimaryCol.identifier(),
|
||||
InstanceDomainIsGeneratedCol.identifier(),
|
||||
InstanceDomainCreationDateCol.identifier(),
|
||||
InstanceDomainChangeDateCol.identifier(),
|
||||
InstanceDomainSequenceCol.identifier(),
|
||||
SecurityPolicyColumnEnabled.identifier(),
|
||||
SecurityPolicyColumnAllowedOrigins.identifier(),
|
||||
LimitsColumnAuditLogRetention.identifier(),
|
||||
LimitsColumnBlock.identifier(),
|
||||
).
|
||||
From(instanceTable.identifier()).
|
||||
LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)).
|
||||
LeftJoin(join(SecurityPolicyColumnInstanceID, InstanceColumnID)).
|
||||
LeftJoin(join(LimitsColumnInstanceID, InstanceColumnID) + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(rows *sql.Rows) (*Instance, error) {
|
||||
instance := &Instance{
|
||||
host: host,
|
||||
Domains: make([]*InstanceDomain, 0),
|
||||
}
|
||||
lang := ""
|
||||
for rows.Next() {
|
||||
var (
|
||||
domain sql.NullString
|
||||
isPrimary sql.NullBool
|
||||
isGenerated sql.NullBool
|
||||
changeDate sql.NullTime
|
||||
creationDate sql.NullTime
|
||||
sequence sql.NullInt64
|
||||
securityPolicyEnabled sql.NullBool
|
||||
auditLogRetention database.NullDuration
|
||||
block sql.NullBool
|
||||
)
|
||||
err := rows.Scan(
|
||||
&instance.ID,
|
||||
&instance.CreationDate,
|
||||
&instance.ChangeDate,
|
||||
&instance.Sequence,
|
||||
&instance.Name,
|
||||
&instance.DefaultOrgID,
|
||||
&instance.IAMProjectID,
|
||||
&instance.ConsoleID,
|
||||
&instance.ConsoleAppID,
|
||||
&lang,
|
||||
&domain,
|
||||
&isPrimary,
|
||||
&isGenerated,
|
||||
&changeDate,
|
||||
&creationDate,
|
||||
&sequence,
|
||||
&securityPolicyEnabled,
|
||||
&instance.csp.allowedOrigins,
|
||||
&auditLogRetention,
|
||||
&block,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-d3fas", "Errors.Internal")
|
||||
}
|
||||
if !domain.Valid {
|
||||
continue
|
||||
}
|
||||
instance.Domains = append(instance.Domains, &InstanceDomain{
|
||||
CreationDate: creationDate.Time,
|
||||
ChangeDate: changeDate.Time,
|
||||
Sequence: uint64(sequence.Int64),
|
||||
Domain: domain.String,
|
||||
IsPrimary: isPrimary.Bool,
|
||||
IsGenerated: isGenerated.Bool,
|
||||
InstanceID: instance.ID,
|
||||
})
|
||||
if auditLogRetention.Valid {
|
||||
instance.auditLogRetention = &auditLogRetention.Duration
|
||||
}
|
||||
if block.Valid {
|
||||
instance.block = &block.Bool
|
||||
}
|
||||
instance.csp.enabled = securityPolicyEnabled.Bool
|
||||
}
|
||||
if instance.ID == "" {
|
||||
return nil, zerrors.ThrowNotFound(nil, "QUERY-1kIjX", "Errors.IAM.NotFound")
|
||||
}
|
||||
instance.DefaultLang = language.Make(lang)
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-Dfbe2", "Errors.Query.CloseRows")
|
||||
}
|
||||
return instance, nil
|
||||
}
|
||||
type authzInstance struct {
|
||||
id string
|
||||
iamProjectID string
|
||||
consoleID string
|
||||
consoleAppID string
|
||||
host string
|
||||
domain string
|
||||
defaultLang language.Tag
|
||||
defaultOrgID string
|
||||
csp csp
|
||||
block *bool
|
||||
auditLogRetention *time.Duration
|
||||
features feature.Features
|
||||
}
|
||||
|
||||
type csp struct {
|
||||
enabled bool
|
||||
allowedOrigins database.TextArray[string]
|
||||
}
|
||||
|
||||
func (i *authzInstance) InstanceID() string {
|
||||
return i.id
|
||||
}
|
||||
|
||||
func (i *authzInstance) ProjectID() string {
|
||||
return i.iamProjectID
|
||||
}
|
||||
|
||||
func (i *authzInstance) ConsoleClientID() string {
|
||||
return i.consoleID
|
||||
}
|
||||
|
||||
func (i *authzInstance) ConsoleApplicationID() string {
|
||||
return i.consoleAppID
|
||||
}
|
||||
|
||||
func (i *authzInstance) RequestedDomain() string {
|
||||
return strings.Split(i.host, ":")[0]
|
||||
}
|
||||
|
||||
func (i *authzInstance) RequestedHost() string {
|
||||
return i.host
|
||||
}
|
||||
|
||||
func (i *authzInstance) DefaultLanguage() language.Tag {
|
||||
return i.defaultLang
|
||||
}
|
||||
|
||||
func (i *authzInstance) DefaultOrganisationID() string {
|
||||
return i.defaultOrgID
|
||||
}
|
||||
|
||||
func (i *authzInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
if !i.csp.enabled {
|
||||
return nil
|
||||
}
|
||||
return i.csp.allowedOrigins
|
||||
}
|
||||
|
||||
func (i *authzInstance) Block() *bool {
|
||||
return i.block
|
||||
}
|
||||
|
||||
func (i *authzInstance) AuditLogRetention() *time.Duration {
|
||||
return i.auditLogRetention
|
||||
}
|
||||
|
||||
func (i *authzInstance) Features() feature.Features {
|
||||
return i.features
|
||||
}
|
||||
|
||||
func scanAuthzInstance(host, domain string) (*authzInstance, func(row *sql.Row) error) {
|
||||
instance := &authzInstance{
|
||||
host: host,
|
||||
domain: domain,
|
||||
}
|
||||
return instance, func(row *sql.Row) error {
|
||||
var (
|
||||
lang string
|
||||
securityPolicyEnabled sql.NullBool
|
||||
auditLogRetention database.NullDuration
|
||||
block sql.NullBool
|
||||
features []byte
|
||||
)
|
||||
err := row.Scan(
|
||||
&instance.id,
|
||||
&instance.defaultOrgID,
|
||||
&instance.iamProjectID,
|
||||
&instance.consoleID,
|
||||
&instance.consoleAppID,
|
||||
&lang,
|
||||
&securityPolicyEnabled,
|
||||
&instance.csp.allowedOrigins,
|
||||
&auditLogRetention,
|
||||
&block,
|
||||
&features,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return zerrors.ThrowNotFound(nil, "QUERY-1kIjX", "Errors.IAM.NotFound")
|
||||
}
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "QUERY-d3fas", "Errors.Internal")
|
||||
}
|
||||
instance.defaultLang = language.Make(lang)
|
||||
if auditLogRetention.Valid {
|
||||
instance.auditLogRetention = &auditLogRetention.Duration
|
||||
}
|
||||
if block.Valid {
|
||||
instance.block = &block.Bool
|
||||
}
|
||||
instance.csp.enabled = securityPolicyEnabled.Bool
|
||||
if len(features) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err = json.Unmarshal(features, &instance.features); err != nil {
|
||||
return zerrors.ThrowInternal(err, "QUERY-Po8ki", "Errors.Internal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
30
internal/query/instance_by_domain.sql
Normal file
30
internal/query/instance_by_domain.sql
Normal file
@ -0,0 +1,30 @@
|
||||
with domain as (
|
||||
select instance_id from projections.instance_domains
|
||||
where domain = $1
|
||||
), features as (
|
||||
select instance_id, json_object_agg(
|
||||
coalesce(i.key, s.key),
|
||||
coalesce(i.value, s.value)
|
||||
) features
|
||||
from domain d
|
||||
cross join projections.system_features s
|
||||
full outer join projections.instance_features i using (key, instance_id)
|
||||
group by instance_id
|
||||
)
|
||||
select
|
||||
i.id,
|
||||
i.default_org_id,
|
||||
i.iam_project_id,
|
||||
i.console_client_id,
|
||||
i.console_app_id,
|
||||
i.default_language,
|
||||
s.enabled,
|
||||
s.origins,
|
||||
l.audit_log_retention,
|
||||
l.block,
|
||||
f.features
|
||||
from domain d
|
||||
join projections.instances i on i.id = d.instance_id
|
||||
left join projections.security_policies s on i.id = s.instance_id
|
||||
left join projections.limits l on i.id = l.instance_id
|
||||
left join features f on i.id = f.instance_id;
|
27
internal/query/instance_by_id.sql
Normal file
27
internal/query/instance_by_id.sql
Normal file
@ -0,0 +1,27 @@
|
||||
with features as (
|
||||
select instance_id, json_object_agg(
|
||||
coalesce(i.key, s.key),
|
||||
coalesce(i.value, s.value)
|
||||
) features
|
||||
from (select $1::text instance_id) x
|
||||
cross join projections.system_features s
|
||||
full outer join projections.instance_features i using (key, instance_id)
|
||||
group by instance_id
|
||||
)
|
||||
select
|
||||
i.id,
|
||||
i.default_org_id,
|
||||
i.iam_project_id,
|
||||
i.console_client_id,
|
||||
i.console_app_id,
|
||||
i.default_language,
|
||||
s.enabled,
|
||||
s.origins,
|
||||
l.audit_log_retention,
|
||||
l.block,
|
||||
f.features
|
||||
from projections.instances i
|
||||
left join projections.security_policies s on i.id = s.instance_id
|
||||
left join projections.limits l on i.id = l.instance_id
|
||||
left join features f on i.id = f.instance_id
|
||||
where i.id = $1;
|
30
internal/query/instance_features.go
Normal file
30
internal/query/instance_features.go
Normal file
@ -0,0 +1,30 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
type InstanceFeatures struct {
|
||||
Details *domain.ObjectDetails
|
||||
LoginDefaultOrg FeatureSource[bool]
|
||||
TriggerIntrospectionProjections FeatureSource[bool]
|
||||
LegacyIntrospection FeatureSource[bool]
|
||||
}
|
||||
|
||||
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {
|
||||
var system *SystemFeatures
|
||||
if cascade {
|
||||
system, err = q.GetSystemFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
m := NewInstanceFeaturesReadModel(ctx, system)
|
||||
if err = q.eventstore.FilterToQueryReducer(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.instance.Details = readModelToObjectDetails(m.ReadModel)
|
||||
return m.instance, nil
|
||||
}
|
109
internal/query/instance_features_model.go
Normal file
109
internal/query/instance_features_model.go
Normal file
@ -0,0 +1,109 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
type InstanceFeaturesReadModel struct {
|
||||
*eventstore.ReadModel
|
||||
system *SystemFeatures
|
||||
instance *InstanceFeatures
|
||||
}
|
||||
|
||||
func NewInstanceFeaturesReadModel(ctx context.Context, system *SystemFeatures) *InstanceFeaturesReadModel {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
m := &InstanceFeaturesReadModel{
|
||||
ReadModel: &eventstore.ReadModel{
|
||||
AggregateID: instanceID,
|
||||
ResourceOwner: instanceID,
|
||||
},
|
||||
instance: new(InstanceFeatures),
|
||||
system: system,
|
||||
}
|
||||
m.populateFromSystem()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesReadModel) Reduce() (err error) {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature_v2.ResetEvent:
|
||||
m.reduceReset()
|
||||
case *feature_v1.SetEvent[feature_v1.Boolean]:
|
||||
err = m.reduceBoolFeature(
|
||||
feature_v1.DefaultLoginInstanceEventToV2(e),
|
||||
)
|
||||
case *feature_v2.SetEvent[bool]:
|
||||
err = m.reduceBoolFeature(e)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return m.ReadModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AddQuery().
|
||||
AggregateTypes(feature_v2.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
EventTypes(
|
||||
feature_v1.DefaultLoginInstanceEventType,
|
||||
feature_v2.InstanceResetEventType,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesReadModel) reduceReset() {
|
||||
if m.populateFromSystem() {
|
||||
return
|
||||
}
|
||||
m.instance.LoginDefaultOrg = FeatureSource[bool]{}
|
||||
m.instance.TriggerIntrospectionProjections = FeatureSource[bool]{}
|
||||
m.instance.LegacyIntrospection = FeatureSource[bool]{}
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesReadModel) populateFromSystem() bool {
|
||||
if m.system == nil {
|
||||
return false
|
||||
}
|
||||
m.instance.LoginDefaultOrg = m.system.LoginDefaultOrg
|
||||
m.instance.TriggerIntrospectionProjections = m.system.TriggerIntrospectionProjections
|
||||
m.instance.LegacyIntrospection = m.system.LegacyIntrospection
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesReadModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
|
||||
level, key, err := event.FeatureInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var dst *FeatureSource[bool]
|
||||
|
||||
switch key {
|
||||
case feature.KeyUnspecified:
|
||||
return nil
|
||||
case feature.KeyLoginDefaultOrg:
|
||||
dst = &m.instance.LoginDefaultOrg
|
||||
case feature.KeyTriggerIntrospectionProjections:
|
||||
dst = &m.instance.TriggerIntrospectionProjections
|
||||
case feature.KeyLegacyIntrospection:
|
||||
dst = &m.instance.LegacyIntrospection
|
||||
}
|
||||
*dst = FeatureSource[bool]{
|
||||
Level: level,
|
||||
Value: event.Value,
|
||||
}
|
||||
return nil
|
||||
}
|
230
internal/query/instance_features_test.go
Normal file
230
internal/query/instance_features_test.go
Normal file
@ -0,0 +1,230 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
func TestQueries_GetInstanceFeatures(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
aggregate := feature_v2.NewAggregate("instance1", "instance1")
|
||||
|
||||
type args struct {
|
||||
cascade bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
args args
|
||||
want *InstanceFeatures
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
args: args{false},
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "system filter error cascaded",
|
||||
args: args{true},
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "no features set, not cascaded",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
want: &InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no features set, cascaded",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectFilter(),
|
||||
),
|
||||
args: args{true},
|
||||
want: &InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
))),
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
),
|
||||
),
|
||||
args: args{true},
|
||||
want: &InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set, reset, set some feature, cascaded",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
))),
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
),
|
||||
),
|
||||
args: args{true},
|
||||
want: &InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set, reset, set some feature, not cascaded",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
),
|
||||
),
|
||||
args: args{false},
|
||||
want: &InstanceFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
q := &Queries{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := q.GetInstanceFeatures(ctx, tt.args.cascade)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -12,33 +12,9 @@ import (
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
instanceQuery = `SELECT projections.instances.id,` +
|
||||
` projections.instances.creation_date,` +
|
||||
` projections.instances.change_date,` +
|
||||
` projections.instances.sequence,` +
|
||||
` projections.instances.default_org_id,` +
|
||||
` projections.instances.iam_project_id,` +
|
||||
` projections.instances.console_client_id,` +
|
||||
` projections.instances.console_app_id,` +
|
||||
` projections.instances.default_language` +
|
||||
` FROM projections.instances` +
|
||||
` AS OF SYSTEM TIME '-1 ms'`
|
||||
instanceCols = []string{
|
||||
"id",
|
||||
"creation_date",
|
||||
"change_date",
|
||||
"sequence",
|
||||
"default_org_id",
|
||||
"iam_project_id",
|
||||
"console_client_id",
|
||||
"console_app_id",
|
||||
"default_language",
|
||||
}
|
||||
instancesQuery = `SELECT f.count, f.id,` +
|
||||
` projections.instances.creation_date,` +
|
||||
` projections.instances.change_date,` +
|
||||
@ -93,76 +69,6 @@ func Test_InstancePrepares(t *testing.T) {
|
||||
want want
|
||||
object interface{}
|
||||
}{
|
||||
{
|
||||
name: "prepareInstanceQuery no result",
|
||||
additionalArgs: []reflect.Value{reflect.ValueOf("")},
|
||||
prepare: prepareInstanceQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueriesScanErr(
|
||||
regexp.QuoteMeta(instanceQuery),
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !zerrors.IsNotFound(err) {
|
||||
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: (*Instance)(nil),
|
||||
},
|
||||
{
|
||||
name: "prepareInstanceQuery found",
|
||||
additionalArgs: []reflect.Value{reflect.ValueOf("")},
|
||||
prepare: prepareInstanceQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQuery(
|
||||
regexp.QuoteMeta(instanceQuery),
|
||||
instanceCols,
|
||||
[]driver.Value{
|
||||
"id",
|
||||
testNow,
|
||||
testNow,
|
||||
uint64(20211108),
|
||||
"global-org-id",
|
||||
"project-id",
|
||||
"client-id",
|
||||
"app-id",
|
||||
"en",
|
||||
},
|
||||
),
|
||||
},
|
||||
object: &Instance{
|
||||
ID: "id",
|
||||
CreationDate: testNow,
|
||||
ChangeDate: testNow,
|
||||
Sequence: 20211108,
|
||||
DefaultOrgID: "global-org-id",
|
||||
IAMProjectID: "project-id",
|
||||
ConsoleID: "client-id",
|
||||
ConsoleAppID: "app-id",
|
||||
DefaultLang: language.English,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prepareInstanceQuery sql err",
|
||||
additionalArgs: []reflect.Value{reflect.ValueOf("")},
|
||||
prepare: prepareInstanceQuery,
|
||||
want: want{
|
||||
sqlExpectations: mockQueryErr(
|
||||
regexp.QuoteMeta(instanceQuery),
|
||||
sql.ErrConnDone,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !errors.Is(err, sql.ErrConnDone) {
|
||||
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: (*Instance)(nil),
|
||||
},
|
||||
{
|
||||
name: "prepareInstancesQuery no result",
|
||||
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) {
|
||||
|
120
internal/query/projection/instance_features.go
Normal file
120
internal/query/projection/instance_features.go
Normal file
@ -0,0 +1,120 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
InstanceFeatureTable = "projections.instance_features"
|
||||
|
||||
InstanceFeatureInstanceIDCol = "instance_id"
|
||||
InstanceFeatureKeyCol = "key"
|
||||
InstanceFeatureCreationDateCol = "creation_date"
|
||||
InstanceFeatureChangeDateCol = "change_date"
|
||||
InstanceFeatureSequenceCol = "sequence"
|
||||
InstanceFeatureValueCol = "value"
|
||||
)
|
||||
|
||||
type instanceFeatureProjection struct{}
|
||||
|
||||
func newInstanceFeatureProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, new(instanceFeatureProjection))
|
||||
}
|
||||
|
||||
func (*instanceFeatureProjection) Name() string {
|
||||
return InstanceFeatureTable
|
||||
}
|
||||
|
||||
func (*instanceFeatureProjection) Init() *old_handler.Check {
|
||||
return handler.NewTableCheck(handler.NewTable(
|
||||
[]*handler.InitColumn{
|
||||
handler.NewColumn(InstanceFeatureInstanceIDCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(InstanceFeatureKeyCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(InstanceFeatureCreationDateCol, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(InstanceFeatureChangeDateCol, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(InstanceFeatureSequenceCol, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(InstanceFeatureValueCol, handler.ColumnTypeJSONB),
|
||||
},
|
||||
handler.NewPrimaryKey(InstanceFeatureInstanceIDCol, InstanceFeatureKeyCol),
|
||||
))
|
||||
}
|
||||
|
||||
func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{{
|
||||
Aggregate: feature_v2.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: feature_v1.DefaultLoginInstanceEventType,
|
||||
Reduce: reduceSetDefaultLoginInstance_v1,
|
||||
},
|
||||
{
|
||||
Event: feature_v2.InstanceResetEventType,
|
||||
Reduce: reduceInstanceResetFeatures,
|
||||
},
|
||||
{
|
||||
Event: feature_v2.InstanceLoginDefaultOrgEventType,
|
||||
Reduce: reduceInstanceSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
|
||||
Reduce: reduceInstanceSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.InstanceLegacyIntrospectionEventType,
|
||||
Reduce: reduceInstanceSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: instance.InstanceRemovedEventType,
|
||||
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func reduceSetDefaultLoginInstance_v1(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*feature_v1.SetEvent[feature_v1.Boolean])
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-in2Xo", "reduce.wrong.event.type %T", event)
|
||||
}
|
||||
return reduceInstanceSetFeature[bool](
|
||||
feature_v1.DefaultLoginInstanceEventToV2(e),
|
||||
)
|
||||
}
|
||||
|
||||
func reduceInstanceSetFeature[T any](event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*feature_v2.SetEvent[T])
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-uPh8O", "reduce.wrong.event.type %T", event)
|
||||
}
|
||||
f, err := e.FeatureJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
columns := []handler.Column{
|
||||
handler.NewCol(InstanceFeatureInstanceIDCol, e.Aggregate().ID),
|
||||
handler.NewCol(InstanceFeatureKeyCol, f.Key.String()),
|
||||
handler.NewCol(InstanceFeatureCreationDateCol, handler.OnlySetValueOnInsert(InstanceFeatureTable, e.CreationDate())),
|
||||
handler.NewCol(InstanceFeatureChangeDateCol, e.CreationDate()),
|
||||
handler.NewCol(InstanceFeatureSequenceCol, e.Sequence()),
|
||||
handler.NewCol(InstanceFeatureValueCol, f.Value),
|
||||
}
|
||||
return handler.NewUpsertStatement(e, columns[0:2], columns), nil
|
||||
}
|
||||
|
||||
func reduceInstanceResetFeatures(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*feature_v2.ResetEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-roo6A", "reduce.wrong.event.type %T", event)
|
||||
}
|
||||
return handler.NewDeleteStatement(e, []handler.Condition{
|
||||
handler.NewCond(InstanceFeatureInstanceIDCol, e.Aggregate().ID),
|
||||
}), nil
|
||||
}
|
152
internal/query/projection/instance_features_test.go
Normal file
152
internal/query/projection/instance_features_test.go
Normal file
@ -0,0 +1,152 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestInstanceFeaturesProjection_reduces(t *testing.T) {
|
||||
type args struct {
|
||||
event func(t *testing.T) eventstore.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
reduce func(event eventstore.Event) (*handler.Statement, error)
|
||||
want wantReduce
|
||||
}{
|
||||
{
|
||||
name: "reduceInstanceSetFeature",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
feature_v2.InstanceLegacyIntrospectionEventType,
|
||||
feature_v2.AggregateType,
|
||||
[]byte(`{"value": true}`),
|
||||
), eventstore.GenericEventMapper[feature_v2.SetEvent[bool]]),
|
||||
},
|
||||
reduce: reduceInstanceSetFeature[bool],
|
||||
want: wantReduce{
|
||||
aggregateType: feature_v2.AggregateType,
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.instance_features (instance_id, key, creation_date, change_date, sequence, value) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (instance_id, key) DO UPDATE SET (creation_date, change_date, sequence, value) = (projections.instance_features.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.value)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
"legacy_introspection",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
[]byte("true"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceSetDefaultLoginInstance_v1",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
feature_v1.DefaultLoginInstanceEventType,
|
||||
feature_v1.AggregateType,
|
||||
[]byte(`{"Value":{"Boolean":true}}`),
|
||||
), eventstore.GenericEventMapper[feature_v1.SetEvent[feature_v1.Boolean]]),
|
||||
},
|
||||
reduce: reduceSetDefaultLoginInstance_v1,
|
||||
want: wantReduce{
|
||||
aggregateType: feature_v2.AggregateType,
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.instance_features (instance_id, key, creation_date, change_date, sequence, value) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (instance_id, key) DO UPDATE SET (creation_date, change_date, sequence, value) = (projections.instance_features.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.value)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
"login_default_org",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
[]byte("true"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceInstanceResetFeatures",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
feature_v2.InstanceResetEventType,
|
||||
feature_v2.AggregateType,
|
||||
[]byte{},
|
||||
), eventstore.GenericEventMapper[feature_v2.ResetEvent]),
|
||||
},
|
||||
reduce: reduceInstanceResetFeatures,
|
||||
want: wantReduce{
|
||||
aggregateType: feature_v2.AggregateType,
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.instance_features WHERE (instance_id = $1)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "instance reduceInstanceRemoved",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
instance.InstanceRemovedEventType,
|
||||
instance.AggregateType,
|
||||
nil,
|
||||
), instance.InstanceRemovedEventMapper),
|
||||
},
|
||||
reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("instance"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.instance_features WHERE (instance_id = $1)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := baseEvent(t)
|
||||
got, err := tt.reduce(event)
|
||||
if ok := zerrors.IsErrorInvalidArgument(err); !ok {
|
||||
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
|
||||
}
|
||||
|
||||
event = tt.args.event(t)
|
||||
got, err = tt.reduce(event)
|
||||
assertReduce(t, got, err, InstanceFeatureTable, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
@ -72,6 +72,8 @@ var (
|
||||
QuotaProjection *quotaProjection
|
||||
LimitsProjection *handler.Handler
|
||||
RestrictionsProjection *handler.Handler
|
||||
SystemFeatureProjection *handler.Handler
|
||||
InstanceFeatureProjection *handler.Handler
|
||||
)
|
||||
|
||||
type projection interface {
|
||||
@ -148,6 +150,8 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
|
||||
QuotaProjection = newQuotaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["quotas"]))
|
||||
LimitsProjection = newLimitsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["limits"]))
|
||||
RestrictionsProjection = newRestrictionsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["restrictions"]))
|
||||
SystemFeatureProjection = newSystemFeatureProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["system_features"]))
|
||||
InstanceFeatureProjection = newInstanceFeatureProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_features"]))
|
||||
newProjectionsList()
|
||||
return nil
|
||||
}
|
||||
@ -257,5 +261,7 @@ func newProjectionsList() {
|
||||
QuotaProjection.handler,
|
||||
LimitsProjection,
|
||||
RestrictionsProjection,
|
||||
SystemFeatureProjection,
|
||||
InstanceFeatureProjection,
|
||||
}
|
||||
}
|
||||
|
98
internal/query/projection/system_features.go
Normal file
98
internal/query/projection/system_features.go
Normal file
@ -0,0 +1,98 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
SystemFeatureTable = "projections.system_features"
|
||||
|
||||
SystemFeatureKeyCol = "key"
|
||||
SystemFeatureCreationDateCol = "creation_date"
|
||||
SystemFeatureChangeDateCol = "change_date"
|
||||
SystemFeatureSequenceCol = "sequence"
|
||||
SystemFeatureValueCol = "value"
|
||||
)
|
||||
|
||||
type systemFeatureProjection struct{}
|
||||
|
||||
func newSystemFeatureProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, new(systemFeatureProjection))
|
||||
}
|
||||
|
||||
func (*systemFeatureProjection) Name() string {
|
||||
return SystemFeatureTable
|
||||
}
|
||||
|
||||
func (*systemFeatureProjection) Init() *old_handler.Check {
|
||||
return handler.NewTableCheck(handler.NewTable(
|
||||
[]*handler.InitColumn{
|
||||
handler.NewColumn(SystemFeatureKeyCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(SystemFeatureCreationDateCol, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(SystemFeatureChangeDateCol, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(SystemFeatureSequenceCol, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(SystemFeatureValueCol, handler.ColumnTypeJSONB),
|
||||
},
|
||||
handler.NewPrimaryKey(SystemFeatureKeyCol),
|
||||
))
|
||||
}
|
||||
|
||||
func (*systemFeatureProjection) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{{
|
||||
Aggregate: feature_v2.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: feature_v2.SystemResetEventType,
|
||||
Reduce: reduceSystemResetFeatures,
|
||||
},
|
||||
{
|
||||
Event: feature_v2.SystemLoginDefaultOrgEventType,
|
||||
Reduce: reduceSystemSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.SystemTriggerIntrospectionProjectionsEventType,
|
||||
Reduce: reduceSystemSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.SystemLegacyIntrospectionEventType,
|
||||
Reduce: reduceSystemSetFeature[bool],
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func reduceSystemSetFeature[T any](event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*feature_v2.SetEvent[T])
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-uPh8O", "reduce.wrong.event.type %T", event)
|
||||
}
|
||||
f, err := e.FeatureJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
columns := []handler.Column{
|
||||
handler.NewCol(SystemFeatureKeyCol, f.Key.String()),
|
||||
handler.NewCol(SystemFeatureCreationDateCol, handler.OnlySetValueOnInsert(SystemFeatureTable, e.CreationDate())),
|
||||
handler.NewCol(SystemFeatureChangeDateCol, e.CreationDate()),
|
||||
handler.NewCol(SystemFeatureSequenceCol, e.Sequence()),
|
||||
handler.NewCol(SystemFeatureValueCol, f.Value),
|
||||
}
|
||||
return handler.NewUpsertStatement(e, columns[0:1], columns), nil
|
||||
}
|
||||
|
||||
func reduceSystemResetFeatures(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*feature_v2.ResetEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-roo6A", "reduce.wrong.event.type %T", event)
|
||||
}
|
||||
return handler.NewDeleteStatement(e, []handler.Condition{
|
||||
// Hack: need at least one condition or the query builder will throw us an error
|
||||
handler.NewIsNotNullCond(SystemFeatureKeyCol),
|
||||
}), nil
|
||||
}
|
90
internal/query/projection/system_features_test.go
Normal file
90
internal/query/projection/system_features_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestSystemFeaturesProjection_reduces(t *testing.T) {
|
||||
type args struct {
|
||||
event func(t *testing.T) eventstore.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
reduce func(event eventstore.Event) (*handler.Statement, error)
|
||||
want wantReduce
|
||||
}{
|
||||
{
|
||||
name: "reduceSystemSetFeature",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
feature_v2.SystemLegacyIntrospectionEventType,
|
||||
feature_v2.AggregateType,
|
||||
[]byte(`{"value": true}`),
|
||||
), eventstore.GenericEventMapper[feature_v2.SetEvent[bool]]),
|
||||
},
|
||||
reduce: reduceSystemSetFeature[bool],
|
||||
want: wantReduce{
|
||||
aggregateType: feature_v2.AggregateType,
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.system_features (key, creation_date, change_date, sequence, value) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (key) DO UPDATE SET (creation_date, change_date, sequence, value) = (projections.system_features.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.value)",
|
||||
expectedArgs: []interface{}{
|
||||
"legacy_introspection",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
[]byte("true"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceSystemResetFeatures",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
feature_v2.SystemResetEventType,
|
||||
feature_v2.AggregateType,
|
||||
[]byte{},
|
||||
), eventstore.GenericEventMapper[feature_v2.ResetEvent]),
|
||||
},
|
||||
reduce: reduceSystemResetFeatures,
|
||||
want: wantReduce{
|
||||
aggregateType: feature_v2.AggregateType,
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.system_features WHERE (key IS NOT NULL)",
|
||||
expectedArgs: []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := baseEvent(t)
|
||||
got, err := tt.reduce(event)
|
||||
if ok := zerrors.IsErrorInvalidArgument(err); !ok {
|
||||
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
|
||||
}
|
||||
|
||||
event = tt.args.event(t)
|
||||
got, err = tt.reduce(event)
|
||||
assertReduce(t, got, err, SystemFeatureTable, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
30
internal/query/system_features.go
Normal file
30
internal/query/system_features.go
Normal file
@ -0,0 +1,30 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
)
|
||||
|
||||
type FeatureSource[T any] struct {
|
||||
Level feature.Level
|
||||
Value T
|
||||
}
|
||||
|
||||
type SystemFeatures struct {
|
||||
Details *domain.ObjectDetails
|
||||
|
||||
LoginDefaultOrg FeatureSource[bool]
|
||||
TriggerIntrospectionProjections FeatureSource[bool]
|
||||
LegacyIntrospection FeatureSource[bool]
|
||||
}
|
||||
|
||||
func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) {
|
||||
m := NewSystemFeaturesReadModel()
|
||||
if err := q.eventstore.FilterToQueryReducer(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.system.Details = readModelToObjectDetails(m.ReadModel)
|
||||
return m.system, nil
|
||||
}
|
82
internal/query/system_features_model.go
Normal file
82
internal/query/system_features_model.go
Normal file
@ -0,0 +1,82 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
type SystemFeaturesReadModel struct {
|
||||
*eventstore.ReadModel
|
||||
system *SystemFeatures
|
||||
}
|
||||
|
||||
func NewSystemFeaturesReadModel() *SystemFeaturesReadModel {
|
||||
m := &SystemFeaturesReadModel{
|
||||
ReadModel: &eventstore.ReadModel{
|
||||
AggregateID: "SYSTEM",
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
system: new(SystemFeatures),
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesReadModel) Reduce() error {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature_v2.ResetEvent:
|
||||
m.reduceReset()
|
||||
case *feature_v2.SetEvent[bool]:
|
||||
err := m.reduceBoolFeature(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return m.ReadModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AddQuery().
|
||||
AggregateTypes(feature_v2.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
EventTypes(
|
||||
feature_v2.SystemResetEventType,
|
||||
feature_v2.SystemLoginDefaultOrgEventType,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType,
|
||||
feature_v2.SystemLegacyIntrospectionEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesReadModel) reduceReset() {
|
||||
m.system = new(SystemFeatures)
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesReadModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
|
||||
level, key, err := event.FeatureInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var dst *FeatureSource[bool]
|
||||
|
||||
switch key {
|
||||
case feature.KeyUnspecified:
|
||||
return nil
|
||||
case feature.KeyLoginDefaultOrg:
|
||||
dst = &m.system.LoginDefaultOrg
|
||||
case feature.KeyTriggerIntrospectionProjections:
|
||||
dst = &m.system.TriggerIntrospectionProjections
|
||||
case feature.KeyLegacyIntrospection:
|
||||
dst = &m.system.LegacyIntrospection
|
||||
}
|
||||
|
||||
*dst = FeatureSource[bool]{
|
||||
Level: level,
|
||||
Value: event.Value,
|
||||
}
|
||||
return nil
|
||||
}
|
179
internal/query/system_features_test.go
Normal file
179
internal/query/system_features_test.go
Normal file
@ -0,0 +1,179 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
func TestQueries_GetSystemFeatures(t *testing.T) {
|
||||
aggregate := feature_v2.NewAggregate("SYSTEM", "SYSTEM")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
want *SystemFeatures
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "no features set",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
want: &SystemFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &SystemFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set, reset, set some feature",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &SystemFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all features set, reset, set some feature, not cascaded",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &SystemFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
LoginDefaultOrg: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
TriggerIntrospectionProjections: FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
LegacyIntrospection: FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
q := &Queries{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := q.GetSystemFeatures(context.Background())
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -13,18 +13,3 @@ const (
|
||||
AggregateType = "feature"
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
// Package feature implements the v1 feature repository.
|
||||
// DEPRECATED: use ./feature_v2 instead.
|
||||
package feature
|
||||
|
||||
import (
|
||||
@ -6,14 +8,22 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultLoginInstanceEventType = EventTypeFromFeature(domain.FeatureLoginDefaultOrg)
|
||||
DefaultLoginInstanceEventType = eventTypePrefix + eventstore.EventType(strings.ToLower("FeatureLoginDefaultOrg")) + setSuffix
|
||||
)
|
||||
|
||||
func EventTypeFromFeature(feature domain.Feature) eventstore.EventType {
|
||||
return eventTypePrefix + eventstore.EventType(strings.ToLower(feature.String())) + setSuffix
|
||||
// DefaultLoginInstanceEventToV2 upgrades the SetEvent to a V2 SetEvent so that
|
||||
// the v2 reducers can handle the V1 events.
|
||||
func DefaultLoginInstanceEventToV2(e *SetEvent[Boolean]) *feature_v2.SetEvent[bool] {
|
||||
v2e := &feature_v2.SetEvent[bool]{
|
||||
BaseEvent: e.BaseEvent,
|
||||
Value: e.Value.Boolean,
|
||||
}
|
||||
v2e.BaseEvent.EventType = feature_v2.InstanceLoginDefaultOrgEventType
|
||||
return v2e
|
||||
}
|
||||
|
||||
type SetEvent[T SetEventType] struct {
|
||||
|
25
internal/repository/feature/feature_v2/aggregate.go
Normal file
25
internal/repository/feature/feature_v2/aggregate.go
Normal file
@ -0,0 +1,25 @@
|
||||
package feature_v2
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
AggregateType = "feature"
|
||||
AggregateVersion = "v2"
|
||||
)
|
||||
|
||||
type Aggregate struct {
|
||||
eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewAggregate(id, resourceOwner string) *Aggregate {
|
||||
return &Aggregate{
|
||||
Aggregate: eventstore.Aggregate{
|
||||
Type: AggregateType,
|
||||
Version: AggregateVersion,
|
||||
ID: id,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
16
internal/repository/feature/feature_v2/eventstore.go
Normal file
16
internal/repository/feature/feature_v2/eventstore.go
Normal file
@ -0,0 +1,16 @@
|
||||
package feature_v2
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
func init() {
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, SystemResetEventType, eventstore.GenericEventMapper[ResetEvent])
|
||||
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, 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]])
|
||||
}
|
132
internal/repository/feature/feature_v2/feature.go
Normal file
132
internal/repository/feature/feature_v2/feature.go
Normal file
@ -0,0 +1,132 @@
|
||||
package feature_v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
SystemResetEventType = resetEventTypeFromFeature(feature.LevelSystem)
|
||||
SystemLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLoginDefaultOrg)
|
||||
SystemTriggerIntrospectionProjectionsEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTriggerIntrospectionProjections)
|
||||
SystemLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLegacyIntrospection)
|
||||
|
||||
InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance)
|
||||
InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg)
|
||||
InstanceTriggerIntrospectionProjectionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTriggerIntrospectionProjections)
|
||||
InstanceLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLegacyIntrospection)
|
||||
)
|
||||
|
||||
const (
|
||||
resetSuffix = "reset"
|
||||
setSuffix = "set"
|
||||
)
|
||||
|
||||
func resetEventTypeFromFeature(level feature.Level) eventstore.EventType {
|
||||
return eventstore.EventType(strings.Join([]string{AggregateType, level.String(), resetSuffix}, "."))
|
||||
}
|
||||
|
||||
func setEventTypeFromFeature(level feature.Level, key feature.Key) eventstore.EventType {
|
||||
return eventstore.EventType(strings.Join([]string{AggregateType, level.String(), key.String(), setSuffix}, "."))
|
||||
}
|
||||
|
||||
type ResetEvent struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *ResetEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
e.BaseEvent = b
|
||||
}
|
||||
|
||||
func (e *ResetEvent) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *ResetEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewResetEvent(
|
||||
ctx context.Context,
|
||||
aggregate *Aggregate,
|
||||
eventType eventstore.EventType,
|
||||
) *ResetEvent {
|
||||
return &ResetEvent{
|
||||
eventstore.NewBaseEventForPush(
|
||||
ctx, &aggregate.Aggregate, eventType),
|
||||
}
|
||||
}
|
||||
|
||||
type SetEvent[T any] struct {
|
||||
*eventstore.BaseEvent `json:"-"`
|
||||
|
||||
Value T
|
||||
}
|
||||
|
||||
func (e *SetEvent[T]) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
e.BaseEvent = b
|
||||
}
|
||||
|
||||
func (e *SetEvent[T]) Payload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *SetEvent[T]) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
type FeatureJSON struct {
|
||||
Key feature.Key
|
||||
Value []byte
|
||||
}
|
||||
|
||||
// FeatureJSON prepares converts the event to a key-value pair with a JSON value payload.
|
||||
func (e *SetEvent[T]) FeatureJSON() (*FeatureJSON, error) {
|
||||
_, key, err := e.FeatureInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jsonValue, err := json.Marshal(e.Value)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternalf(err, "FEAT-go9Ji", "reduce.wrong.event.type %s", e.EventType)
|
||||
}
|
||||
return &FeatureJSON{
|
||||
Key: key,
|
||||
Value: jsonValue,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FeatureInfo extracts a feature's level and key from the event.
|
||||
func (e *SetEvent[T]) FeatureInfo() (feature.Level, feature.Key, error) {
|
||||
ss := strings.Split(string(e.EventType), ".")
|
||||
if len(ss) != 4 {
|
||||
return 0, 0, zerrors.ThrowInternalf(nil, "FEAT-Ahs4m", "reduce.wrong.event.type %s", e.EventType)
|
||||
}
|
||||
level, err := feature.LevelString(ss[1])
|
||||
if err != nil {
|
||||
return 0, 0, zerrors.ThrowInternalf(err, "FEAT-Boo2i", "reduce.wrong.event.type %s", e.EventType)
|
||||
}
|
||||
key, err := feature.KeyString(ss[2])
|
||||
if err != nil {
|
||||
return 0, 0, zerrors.ThrowInternalf(err, "FEAT-eir0M", "reduce.wrong.event.type %s", e.EventType)
|
||||
}
|
||||
return level, key, nil
|
||||
}
|
||||
|
||||
func NewSetEvent[T any](
|
||||
ctx context.Context,
|
||||
aggregate *Aggregate,
|
||||
eventType eventstore.EventType,
|
||||
value T,
|
||||
) *SetEvent[T] {
|
||||
return &SetEvent[T]{
|
||||
eventstore.NewBaseEventForPush(
|
||||
ctx, &aggregate.Aggregate, eventType),
|
||||
value,
|
||||
}
|
||||
}
|
118
internal/repository/feature/feature_v2/feature_test.go
Normal file
118
internal/repository/feature/feature_v2/feature_test.go
Normal file
@ -0,0 +1,118 @@
|
||||
package feature_v2
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestSetEvent_FeatureJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
e *SetEvent[float64] // using float so it's easy to create marshal errors
|
||||
want *FeatureJSON
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid key error",
|
||||
e: &SetEvent[float64]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: "feature.system.foo_bar.some_feat",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInternalf(nil, "FEAT-eir0M", "reduce.wrong.event.type %s", "feature.system.foo_bar.some_feat"),
|
||||
},
|
||||
{
|
||||
name: "marshal error",
|
||||
e: &SetEvent[float64]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: SystemLoginDefaultOrgEventType,
|
||||
},
|
||||
Value: math.NaN(),
|
||||
},
|
||||
wantErr: zerrors.ThrowInternalf(nil, "FEAT-go9Ji", "reduce.wrong.event.type %s", SystemLoginDefaultOrgEventType),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
e: &SetEvent[float64]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: SystemLoginDefaultOrgEventType,
|
||||
},
|
||||
Value: 555,
|
||||
},
|
||||
want: &FeatureJSON{
|
||||
Key: feature.KeyLoginDefaultOrg,
|
||||
Value: []byte(`555`),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.e.FeatureJSON()
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEvent_FeatureInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
e *SetEvent[bool]
|
||||
want feature.Level
|
||||
want1 feature.Key
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "format error",
|
||||
e: &SetEvent[bool]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: "foo.bar",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInternalf(nil, "FEAT-Ahs4m", "reduce.wrong.event.type %s", "foo.bar"),
|
||||
},
|
||||
{
|
||||
name: "level error",
|
||||
e: &SetEvent[bool]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: "feature.foo.bar.something",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInternalf(nil, "FEAT-Boo2i", "reduce.wrong.event.type %s", "feature.foo.bar.something"),
|
||||
},
|
||||
{
|
||||
name: "key error",
|
||||
e: &SetEvent[bool]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: "feature.system.bar.something",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInternalf(nil, "FEAT-eir0M", "reduce.wrong.event.type %s", "feature.system.bar.something"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
e: &SetEvent[bool]{
|
||||
BaseEvent: &eventstore.BaseEvent{
|
||||
EventType: SystemLoginDefaultOrgEventType,
|
||||
},
|
||||
},
|
||||
want: feature.LevelSystem,
|
||||
want1: feature.KeyLoginDefaultOrg,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := tt.e.FeatureInfo()
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
assert.Equal(t, tt.want1, got1)
|
||||
})
|
||||
}
|
||||
}
|
35
proto/zitadel/feature/v2beta/feature.proto
Normal file
35
proto/zitadel/feature/v2beta/feature.proto
Normal file
@ -0,0 +1,35 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package zitadel.feature.v2beta;
|
||||
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
|
||||
|
||||
|
||||
enum Source {
|
||||
SOURCE_UNSPECIFIED = 0;
|
||||
reserved 1; // in case we want to implement a "DEFAULT" level
|
||||
SOURCE_SYSTEM = 2;
|
||||
SOURCE_INSTANCE = 3;
|
||||
SOURCE_ORGANIZATION = 4;
|
||||
SOURCE_PROJECT = 5; // reserved for future use
|
||||
SOURCE_APP = 6; // reserved for future use
|
||||
SOURCE_USER = 7;
|
||||
}
|
||||
|
||||
// FeatureFlag is a simple boolean Feature setting, without further payload.
|
||||
message FeatureFlag {
|
||||
bool enabled = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "false";
|
||||
description: "Whether a feature is enabled.";
|
||||
}
|
||||
];
|
||||
|
||||
Source source = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "The source where the setting of the feature was defined. The source may be the resource itself or a resource owner through inheritance.";
|
||||
}
|
||||
];
|
||||
}
|
395
proto/zitadel/feature/v2beta/feature_service.proto
Normal file
395
proto/zitadel/feature/v2beta/feature_service.proto
Normal file
@ -0,0 +1,395 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package zitadel.feature.v2beta;
|
||||
|
||||
import "google/api/annotations.proto";
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
|
||||
import "zitadel/feature/v2beta/system.proto";
|
||||
import "zitadel/feature/v2beta/instance.proto";
|
||||
import "zitadel/feature/v2beta/organization.proto";
|
||||
import "zitadel/feature/v2beta/user.proto";
|
||||
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
||||
info: {
|
||||
title: "Feature Service";
|
||||
version: "2.0-beta";
|
||||
description: "This API is intended to manage features for ZITADEL. Feature settings that are available on multiple \"levels\", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op. This project is in beta state. It can AND will continue breaking until a stable version is released.";
|
||||
contact:{
|
||||
name: "ZITADEL"
|
||||
url: "https://zitadel.com"
|
||||
email: "hi@zitadel.com"
|
||||
}
|
||||
license: {
|
||||
name: "Apache 2.0",
|
||||
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
|
||||
};
|
||||
};
|
||||
schemes: HTTPS;
|
||||
schemes: HTTP;
|
||||
|
||||
consumes: "application/json";
|
||||
consumes: "application/grpc";
|
||||
|
||||
produces: "application/json";
|
||||
produces: "application/grpc";
|
||||
|
||||
consumes: "application/grpc-web+proto";
|
||||
produces: "application/grpc-web+proto";
|
||||
|
||||
host: "$CUSTOM-DOMAIN";
|
||||
base_path: "/";
|
||||
|
||||
external_docs: {
|
||||
description: "Detailed information about ZITADEL",
|
||||
url: "https://zitadel.com/docs"
|
||||
}
|
||||
security_definitions: {
|
||||
security: {
|
||||
key: "OAuth2";
|
||||
value: {
|
||||
type: TYPE_OAUTH2;
|
||||
flow: FLOW_ACCESS_CODE;
|
||||
authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
|
||||
token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
|
||||
scopes: {
|
||||
scope: {
|
||||
key: "openid";
|
||||
value: "openid";
|
||||
}
|
||||
scope: {
|
||||
key: "urn:zitadel:iam:org:project:id:zitadel:aud";
|
||||
value: "urn:zitadel:iam:org:project:id:zitadel:aud";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
security: {
|
||||
security_requirement: {
|
||||
key: "OAuth2";
|
||||
value: {
|
||||
scope: "openid";
|
||||
scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
key: "403";
|
||||
value: {
|
||||
description: "Returned when the user does not have permission to access the resource.";
|
||||
schema: {
|
||||
json_schema: {
|
||||
ref: "#/definitions/rpcStatus";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
key: "404";
|
||||
value: {
|
||||
description: "Returned when the resource has no feature flag settings and inheritance from the parent is disabled.";
|
||||
schema: {
|
||||
json_schema: {
|
||||
ref: "#/definitions/rpcStatus";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// FeatureService is intended to manage features for ZITADEL.
|
||||
//
|
||||
// Feature settings that are available on multiple "levels", such as instance and organization.
|
||||
// The higher level (instance) acts as a default for the lower level (organization).
|
||||
// When a feature is set on multiple levels, the lower level takes precedence.
|
||||
//
|
||||
// Features can be experimental where ZITADEL will assume a sane default, such as disabled.
|
||||
// When over time confidence in such a feature grows, ZITADEL can default to enabling the feature.
|
||||
// As a final step we might choose to always enable a feature and remove the setting from this API,
|
||||
// reserving the proto field number. Such removal is not considered a breaking change.
|
||||
// Setting a removed field will effectively result in a no-op.
|
||||
service FeatureService {
|
||||
rpc SetSystemFeatures (SetSystemFeaturesRequest) returns (SetSystemFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
put: "/v2beta/features/system"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "system.feature.write"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Set system level features";
|
||||
description: "Configure and set features that apply to the complete system. Only fields present in the request are set or unset."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc ResetSystemFeatures (ResetSystemFeaturesRequest) returns (ResetSystemFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/v2beta/features/system"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "system.feature.delete"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Reset system level features";
|
||||
description: "Deletes ALL configured features for the system, reverting the behaviors to system defaults."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetSystemFeatures (GetSystemFeaturesRequest) returns (GetSystemFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/v2beta/features/system"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "system.feature.read"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Get system level features";
|
||||
description: "Returns all configured features for the system. Unset fields mean the feature is the current system default."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc SetInstanceFeatures (SetInstanceFeaturesRequest) returns (SetInstanceFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
put: "/v2beta/features/instance"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "iam.feature.write"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Set instance level features";
|
||||
description: "Configure and set features that apply to a complete instance. Only fields present in the request are set or unset."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc ResetInstanceFeatures (ResetInstanceFeaturesRequest) returns (ResetInstanceFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/v2beta/features/instance"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "iam.feature.delete"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Reset instance level features";
|
||||
description: "Deletes ALL configured features for an instance, reverting the behaviors to system defaults."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetInstanceFeatures (GetInstanceFeaturesRequest) returns (GetInstanceFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/v2beta/features/instance"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "iam.feature.read"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Get instance level features";
|
||||
description: "Returns all configured features for an instance. Unset fields mean the feature is the current system default."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc SetOrganizationFeatures (SetOrganizationFeaturesRequest) returns (SetOrganizationFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
put: "/v2beta/features/organization/{organization_id}"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "org.feature.write"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Set organization level features";
|
||||
description: "Configure and set features that apply to a complete instance. Only fields present in the request are set or unset."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc ResetOrganizationFeatures (ResetOrganizationFeaturesRequest) returns (ResetOrganizationFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/v2beta/features/organization/{organization_id}"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "org.feature.write"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Reset organization level features";
|
||||
description: "Deletes ALL configured features for an organization, reverting the behaviors to instance defaults."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetOrganizationFeatures(GetOrganizationFeaturesRequest) returns (GetOrganizationFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/v2beta/features/organization/{organization_id}"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "org.feature.read"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Get organization level features";
|
||||
description: "Returns all configured features for an organization. Unset fields mean the feature is the current instance default."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc SetUserFeatures(SetUserFeatureRequest) returns (SetUserFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
put: "/v2beta/features/user/{user_id}"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "user.feature.write"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Set user level features";
|
||||
description: "Configure and set features that apply to an user. Only fields present in the request are set or unset."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc ResetUserFeatures(ResetUserFeaturesRequest) returns (ResetUserFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/v2beta/features/user/{user_id}"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "user.feature.write"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Reset user level features";
|
||||
description: "Deletes ALL configured features for a user, reverting the behaviors to organization defaults."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetUserFeatures(GetUserFeaturesRequest) returns (GetUserFeaturesResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/v2beta/features/user/{user_id}"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "user.feature.read"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Get organization level features";
|
||||
description: "Returns all configured features for an organization. Unset fields mean the feature is the current instance default."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
76
proto/zitadel/feature/v2beta/instance.proto
Normal file
76
proto/zitadel/feature/v2beta/instance.proto
Normal file
@ -0,0 +1,76 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package zitadel.feature.v2beta;
|
||||
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
|
||||
import "zitadel/object/v2beta/object.proto";
|
||||
import "zitadel/feature/v2beta/feature.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
|
||||
|
||||
message SetInstanceFeaturesRequest{
|
||||
optional bool login_default_org = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set";
|
||||
}
|
||||
];
|
||||
optional bool oidc_trigger_introspection_projections = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
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.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetInstanceFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message ResetInstanceFeaturesRequest {}
|
||||
|
||||
message ResetInstanceFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message GetInstanceFeaturesRequest {
|
||||
bool inheritance = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "Inherit unset features from the resource owners. This option is recursive: if the flag is set, the resource's ancestors are consulted up to system defaults. If this option is disabled and the feature is not set on the instance, it will be omitted from the response or Not Found is returned when the instance has no features flags at all.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message GetInstanceFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
FeatureFlag login_default_org = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set";
|
||||
}
|
||||
];
|
||||
|
||||
FeatureFlag oidc_trigger_introspection_projections = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
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.";
|
||||
}
|
||||
];
|
||||
|
||||
FeatureFlag oidc_legacy_introspection = 4 [
|
||||
(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.";
|
||||
}
|
||||
];
|
||||
}
|
62
proto/zitadel/feature/v2beta/organization.proto
Normal file
62
proto/zitadel/feature/v2beta/organization.proto
Normal file
@ -0,0 +1,62 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package zitadel.feature.v2beta;
|
||||
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
|
||||
import "zitadel/object/v2beta/object.proto";
|
||||
import "zitadel/feature/v2beta/feature.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
|
||||
|
||||
message SetOrganizationFeaturesRequest {
|
||||
string organization_id = 1[
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629023906488334\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetOrganizationFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message ResetOrganizationFeaturesRequest {
|
||||
string organization_id = 1[
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629023906488334\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message ResetOrganizationFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message GetOrganizationFeaturesRequest {
|
||||
string organization_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629023906488334\"";
|
||||
}
|
||||
];
|
||||
bool inheritance = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "Inherit unset features from the resource owners. This option is recursive: if the flag is set, the resource's ancestors are consulted up to system defaults. If this option is disabled and the feature is not set on the organization, it will be omitted from the response or Not Found is returned when the organization has no features flags at all.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message GetOrganizationFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
70
proto/zitadel/feature/v2beta/system.proto
Normal file
70
proto/zitadel/feature/v2beta/system.proto
Normal file
@ -0,0 +1,70 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package zitadel.feature.v2beta;
|
||||
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
|
||||
import "zitadel/object/v2beta/object.proto";
|
||||
import "zitadel/feature/v2beta/feature.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
|
||||
|
||||
message SetSystemFeaturesRequest{
|
||||
optional bool login_default_org = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set";
|
||||
}
|
||||
];
|
||||
|
||||
optional bool oidc_trigger_introspection_projections = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
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.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetSystemFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message ResetSystemFeaturesRequest {}
|
||||
|
||||
message ResetSystemFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message GetSystemFeaturesRequest {}
|
||||
|
||||
message GetSystemFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
FeatureFlag login_default_org = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set";
|
||||
}
|
||||
];
|
||||
|
||||
FeatureFlag oidc_trigger_introspection_projections = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
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.";
|
||||
}
|
||||
];
|
||||
|
||||
FeatureFlag oidc_legacy_introspection = 4 [
|
||||
(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.";
|
||||
}
|
||||
];
|
||||
}
|
62
proto/zitadel/feature/v2beta/user.proto
Normal file
62
proto/zitadel/feature/v2beta/user.proto
Normal file
@ -0,0 +1,62 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package zitadel.feature.v2beta;
|
||||
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
|
||||
import "zitadel/object/v2beta/object.proto";
|
||||
import "zitadel/feature/v2beta/feature.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature";
|
||||
|
||||
message SetUserFeatureRequest {
|
||||
string user_id = 1[
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629023906488334\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetUserFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message ResetUserFeaturesRequest {
|
||||
string user_id = 1[
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629023906488334\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message ResetUserFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message GetUserFeaturesRequest {
|
||||
string user_id = 1[
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629023906488334\"";
|
||||
}
|
||||
];
|
||||
bool inheritance = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "Inherit unset features from the resource owners. This option is recursive: if the flag is set, the resource's ancestors are consulted up to system defaults. If this option is disabled and the feature is not set on the user, it will be ommitted from the response or Not Found is returned when the user has no features flags at all.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message GetUserFeaturesResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user