mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 20:37:30 +00:00
feat(login): use default org for login without provided org context (#6625)
* start feature flags * base feature events on domain const * setup default features * allow setting feature in system api * allow setting feature in admin api * set settings in login based on feature * fix rebasing * unit tests * i18n * update policy after domain discovery * some changes from review * check feature and value type * check feature and value type
This commit is contained in:
@@ -761,6 +761,8 @@ DefaultInstance:
|
|||||||
Greeting: Hello {{.DisplayName}},
|
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.
|
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
|
ButtonText: Login
|
||||||
|
Features:
|
||||||
|
- FeatureLoginDefaultOrg: true
|
||||||
|
|
||||||
Quotas:
|
Quotas:
|
||||||
# Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist.
|
# Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist.
|
||||||
@@ -819,6 +821,7 @@ InternalAuthZ:
|
|||||||
- "iam.flow.read"
|
- "iam.flow.read"
|
||||||
- "iam.flow.write"
|
- "iam.flow.write"
|
||||||
- "iam.flow.delete"
|
- "iam.flow.delete"
|
||||||
|
- "iam.feature.write"
|
||||||
- "org.read"
|
- "org.read"
|
||||||
- "org.global.read"
|
- "org.global.read"
|
||||||
- "org.create"
|
- "org.create"
|
||||||
|
@@ -23,6 +23,7 @@ func MustNewConfig(v *viper.Viper) *Config {
|
|||||||
mapstructure.StringToTimeDurationHookFunc(),
|
mapstructure.StringToTimeDurationHookFunc(),
|
||||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||||
mapstructure.StringToSliceHookFunc(","),
|
mapstructure.StringToSliceHookFunc(","),
|
||||||
|
hook.StringToFeatureHookFunc(),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
logging.OnError(err).Fatal("unable to read default config")
|
logging.OnError(err).Fatal("unable to read default config")
|
||||||
|
@@ -24,6 +24,7 @@ type FirstInstance struct {
|
|||||||
Org command.InstanceOrgSetup
|
Org command.InstanceOrgSetup
|
||||||
MachineKeyPath string
|
MachineKeyPath string
|
||||||
PatPath string
|
PatPath string
|
||||||
|
Features map[domain.Feature]any
|
||||||
|
|
||||||
instanceSetup command.InstanceSetup
|
instanceSetup command.InstanceSetup
|
||||||
userEncryptionKey *crypto.KeyConfig
|
userEncryptionKey *crypto.KeyConfig
|
||||||
|
@@ -43,6 +43,7 @@ func MustNewConfig(v *viper.Viper) *Config {
|
|||||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||||
mapstructure.StringToSliceHookFunc(","),
|
mapstructure.StringToSliceHookFunc(","),
|
||||||
database.DecodeHook,
|
database.DecodeHook,
|
||||||
|
hook.StringToFeatureHookFunc(),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
logging.OnError(err).Fatal("unable to read default config")
|
logging.OnError(err).Fatal("unable to read default config")
|
||||||
@@ -99,6 +100,7 @@ func MustNewSteps(v *viper.Viper) *Steps {
|
|||||||
mapstructure.StringToTimeDurationHookFunc(),
|
mapstructure.StringToTimeDurationHookFunc(),
|
||||||
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
mapstructure.StringToTimeHookFunc(time.RFC3339),
|
||||||
mapstructure.StringToSliceHookFunc(","),
|
mapstructure.StringToSliceHookFunc(","),
|
||||||
|
hook.StringToFeatureHookFunc(),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
logging.OnError(err).Fatal("unable to read steps")
|
logging.OnError(err).Fatal("unable to read steps")
|
||||||
|
@@ -92,6 +92,7 @@ func MustNewConfig(v *viper.Viper) *Config {
|
|||||||
database.DecodeHook,
|
database.DecodeHook,
|
||||||
actions.HTTPConfigDecodeHook,
|
actions.HTTPConfigDecodeHook,
|
||||||
systemAPIUsersDecodeHook,
|
systemAPIUsersDecodeHook,
|
||||||
|
hook.StringToFeatureHookFunc(),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
logging.OnError(err).Fatal("unable to read config")
|
logging.OnError(err).Fatal("unable to read config")
|
||||||
|
@@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/cmd/build"
|
"github.com/zitadel/zitadel/cmd/build"
|
||||||
"github.com/zitadel/zitadel/cmd/key"
|
"github.com/zitadel/zitadel/cmd/key"
|
||||||
cmd_tls "github.com/zitadel/zitadel/cmd/tls"
|
cmd_tls "github.com/zitadel/zitadel/cmd/tls"
|
||||||
|
"github.com/zitadel/zitadel/feature"
|
||||||
"github.com/zitadel/zitadel/internal/actions"
|
"github.com/zitadel/zitadel/internal/actions"
|
||||||
admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
||||||
"github.com/zitadel/zitadel/internal/api"
|
"github.com/zitadel/zitadel/internal/api"
|
||||||
@@ -412,7 +413,27 @@ func startAPIs(
|
|||||||
}
|
}
|
||||||
apis.RegisterHandlerOnPrefix(console.HandlerPrefix, c)
|
apis.RegisterHandlerOnPrefix(console.HandlerPrefix, c)
|
||||||
|
|
||||||
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), provider.AuthCallbackURL(samlProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, provider.NewIssuerInterceptor(samlProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.WithoutLimiting().Handle, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
|
l, err := login.CreateLogin(
|
||||||
|
config.Login,
|
||||||
|
commands,
|
||||||
|
queries,
|
||||||
|
authRepo,
|
||||||
|
store,
|
||||||
|
console.HandlerPrefix+"/",
|
||||||
|
op.AuthCallbackURL(oidcProvider),
|
||||||
|
provider.AuthCallbackURL(samlProvider),
|
||||||
|
config.ExternalSecure,
|
||||||
|
userAgentInterceptor,
|
||||||
|
op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler,
|
||||||
|
provider.NewIssuerInterceptor(samlProvider.IssuerFromRequest).Handler,
|
||||||
|
instanceInterceptor.Handler,
|
||||||
|
assetsCache.Handler,
|
||||||
|
limitingAccessInterceptor.WithoutLimiting().Handle,
|
||||||
|
keys.User,
|
||||||
|
keys.IDPConfig,
|
||||||
|
keys.CSRFCookieKey,
|
||||||
|
feature.NewCheck(eventstore),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to start login: %w", err)
|
return fmt.Errorf("unable to start login: %w", err)
|
||||||
}
|
}
|
||||||
|
45
feature/check.go
Normal file
45
feature/check.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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
|
||||||
|
}
|
4
go.mod
4
go.mod
@@ -87,6 +87,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.43.1 // indirect
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.43.1 // indirect
|
||||||
|
github.com/dmarkham/enumer v1.5.8 // indirect
|
||||||
github.com/go-logr/logr v1.2.4 // indirect
|
github.com/go-logr/logr v1.2.4 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||||
@@ -101,9 +102,12 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/muhlemmer/httpforwarded v0.1.0 // indirect
|
github.com/muhlemmer/httpforwarded v0.1.0 // indirect
|
||||||
|
github.com/pascaldekloe/name v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||||
github.com/smartystreets/assertions v1.0.0 // indirect
|
github.com/smartystreets/assertions v1.0.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
|
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
|
||||||
|
golang.org/x/mod v0.12.0 // indirect
|
||||||
|
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
|
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||||
)
|
)
|
||||||
|
8
go.sum
8
go.sum
@@ -179,6 +179,8 @@ github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwu
|
|||||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/dmarkham/enumer v1.5.8 h1:fIF11F9l5jyD++YYvxcSH5WgHfeaSGPaN/T4kOQ4qEM=
|
||||||
|
github.com/dmarkham/enumer v1.5.8/go.mod h1:d10o8R3t/gROm2p3BXqTkMt2+HMuxEmWCXzorAruYak=
|
||||||
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
||||||
github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||||
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog=
|
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog=
|
||||||
@@ -709,6 +711,8 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh
|
|||||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U=
|
||||||
|
github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM=
|
||||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
@@ -989,6 +993,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -1222,6 +1228,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
|||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
|
||||||
|
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
20
internal/api/grpc/admin/feature.go
Normal file
20
internal/api/grpc/admin/feature.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
object_pb "github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &admin_pb.ActivateFeatureLoginDefaultOrgResponse{
|
||||||
|
Details: object_pb.DomainToChangeDetailsPb(details),
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
34
internal/api/grpc/system/feature.go
Normal file
34
internal/api/grpc/system/feature.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
object_pb "github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
|
system_pb "github.com/zitadel/zitadel/pkg/grpc/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) SetInstanceFeature(ctx context.Context, req *system_pb.SetInstanceFeatureRequest) (*system_pb.SetInstanceFeatureResponse, error) {
|
||||||
|
details, err := s.setInstanceFeature(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &system_pb.SetInstanceFeatureResponse{
|
||||||
|
Details: object_pb.DomainToChangeDetailsPb(details),
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) setInstanceFeature(ctx context.Context, req *system_pb.SetInstanceFeatureRequest) (*domain.ObjectDetails, error) {
|
||||||
|
feat := domain.Feature(req.FeatureId)
|
||||||
|
if !feat.IsAFeature() {
|
||||||
|
return nil, errors.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)
|
||||||
|
default:
|
||||||
|
return nil, errors.ThrowInvalidArgument(nil, "SYST-dag5g", "Errors.Feature.TypeNotSupported")
|
||||||
|
}
|
||||||
|
}
|
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/rakyll/statik/fs"
|
"github.com/rakyll/statik/fs"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/feature"
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||||
@@ -40,6 +41,7 @@ type Login struct {
|
|||||||
samlAuthCallbackURL func(context.Context, string) string
|
samlAuthCallbackURL func(context.Context, string) string
|
||||||
idpConfigAlg crypto.EncryptionAlgorithm
|
idpConfigAlg crypto.EncryptionAlgorithm
|
||||||
userCodeAlg crypto.EncryptionAlgorithm
|
userCodeAlg crypto.EncryptionAlgorithm
|
||||||
|
featureCheck feature.Checker
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -76,6 +78,7 @@ func CreateLogin(config Config,
|
|||||||
userCodeAlg crypto.EncryptionAlgorithm,
|
userCodeAlg crypto.EncryptionAlgorithm,
|
||||||
idpConfigAlg crypto.EncryptionAlgorithm,
|
idpConfigAlg crypto.EncryptionAlgorithm,
|
||||||
csrfCookieKey []byte,
|
csrfCookieKey []byte,
|
||||||
|
featureCheck feature.Checker,
|
||||||
) (*Login, error) {
|
) (*Login, error) {
|
||||||
login := &Login{
|
login := &Login{
|
||||||
oidcAuthCallbackURL: oidcAuthCallbackURL,
|
oidcAuthCallbackURL: oidcAuthCallbackURL,
|
||||||
@@ -88,6 +91,7 @@ func CreateLogin(config Config,
|
|||||||
authRepo: authRepo,
|
authRepo: authRepo,
|
||||||
idpConfigAlg: idpConfigAlg,
|
idpConfigAlg: idpConfigAlg,
|
||||||
userCodeAlg: userCodeAlg,
|
userCodeAlg: userCodeAlg,
|
||||||
|
featureCheck: featureCheck,
|
||||||
}
|
}
|
||||||
statikFS, err := fs.NewWithNamespace("login")
|
statikFS, err := fs.NewWithNamespace("login")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -506,25 +506,19 @@ func (l *Login) getOrgID(r *http.Request, authReq *domain.AuthRequest) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Login) getPrivateLabelingID(r *http.Request, authReq *domain.AuthRequest) string {
|
func (l *Login) getPrivateLabelingID(r *http.Request, authReq *domain.AuthRequest) string {
|
||||||
privateLabelingOrgID := authz.GetInstance(r.Context()).InstanceID()
|
defaultID := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||||
if authReq == nil {
|
f, err := l.featureCheck.CheckInstanceBooleanFeature(r.Context(), domain.FeatureLoginDefaultOrg)
|
||||||
if id := r.FormValue(queryOrgID); id != "" {
|
logging.OnError(err).Warnf("could not check feature %s", domain.FeatureLoginDefaultOrg)
|
||||||
return id
|
if !f.Boolean {
|
||||||
}
|
defaultID = authz.GetInstance(r.Context()).InstanceID()
|
||||||
return privateLabelingOrgID
|
|
||||||
}
|
}
|
||||||
if authReq.PrivateLabelingSetting != domain.PrivateLabelingSettingUnspecified {
|
if authReq != nil {
|
||||||
privateLabelingOrgID = authReq.ApplicationResourceOwner
|
return authReq.PrivateLabelingOrgID(defaultID)
|
||||||
}
|
}
|
||||||
if authReq.PrivateLabelingSetting == domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy || authReq.PrivateLabelingSetting == domain.PrivateLabelingSettingUnspecified {
|
if id := r.FormValue(queryOrgID); id != "" {
|
||||||
if authReq.UserOrgID != "" {
|
return id
|
||||||
privateLabelingOrgID = authReq.UserOrgID
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if authReq.RequestedOrgID != "" {
|
return defaultID
|
||||||
privateLabelingOrgID = authReq.RequestedOrgID
|
|
||||||
}
|
|
||||||
return privateLabelingOrgID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Login) getOrgName(authReq *domain.AuthRequest) string {
|
func (l *Login) getOrgName(authReq *domain.AuthRequest) string {
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/feature"
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
|
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
|
||||||
cache "github.com/zitadel/zitadel/internal/auth_request/repository"
|
cache "github.com/zitadel/zitadel/internal/auth_request/repository"
|
||||||
@@ -52,6 +53,8 @@ type AuthRequestRepo struct {
|
|||||||
ApplicationProvider applicationProvider
|
ApplicationProvider applicationProvider
|
||||||
CustomTextProvider customTextProvider
|
CustomTextProvider customTextProvider
|
||||||
|
|
||||||
|
FeatureCheck feature.Checker
|
||||||
|
|
||||||
IdGenerator id.Generator
|
IdGenerator id.Generator
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,7 +653,12 @@ func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.A
|
|||||||
orgID = request.UserOrgID
|
orgID = request.UserOrgID
|
||||||
}
|
}
|
||||||
if orgID == "" {
|
if orgID == "" {
|
||||||
orgID = authz.GetInstance(ctx).InstanceID()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loginPolicy, idpProviders, err := repo.getLoginPolicyAndIDPProviders(ctx, orgID)
|
loginPolicy, idpProviders, err := repo.getLoginPolicyAndIDPProviders(ctx, orgID)
|
||||||
@@ -671,19 +679,7 @@ func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.A
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
request.PrivacyPolicy = privacyPolicy
|
request.PrivacyPolicy = privacyPolicy
|
||||||
privateLabelingOrgID := authz.GetInstance(ctx).InstanceID()
|
labelPolicy, err := repo.getLabelPolicy(ctx, request.PrivateLabelingOrgID(orgID))
|
||||||
if request.PrivateLabelingSetting != domain.PrivateLabelingSettingUnspecified {
|
|
||||||
privateLabelingOrgID = request.ApplicationResourceOwner
|
|
||||||
}
|
|
||||||
if request.PrivateLabelingSetting == domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy || request.PrivateLabelingSetting == domain.PrivateLabelingSettingUnspecified {
|
|
||||||
if request.UserOrgID != "" {
|
|
||||||
privateLabelingOrgID = request.UserOrgID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if request.RequestedOrgID != "" {
|
|
||||||
privateLabelingOrgID = request.RequestedOrgID
|
|
||||||
}
|
|
||||||
labelPolicy, err := repo.getLabelPolicy(ctx, privateLabelingOrgID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -749,8 +745,9 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain
|
|||||||
}
|
}
|
||||||
// the user was either not found or not active
|
// the user was either not found or not active
|
||||||
// so check if the loginname suffix matches a verified org domain
|
// so check if the loginname suffix matches a verified org domain
|
||||||
if repo.checkDomainDiscovery(ctx, request, loginName) {
|
ok, err := repo.checkDomainDiscovery(ctx, request, loginName)
|
||||||
return nil
|
if err != nil || ok {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
// let's once again check if the user was just inactive
|
// let's once again check if the user was just inactive
|
||||||
if user != nil && user.State == int32(domain.UserStateInactive) {
|
if user != nil && user.State == int32(domain.UserStateInactive) {
|
||||||
@@ -782,30 +779,34 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain
|
|||||||
return errors.ThrowInternal(nil, "AUTH-asf3df", "Errors.Internal")
|
return errors.ThrowInternal(nil, "AUTH-asf3df", "Errors.Internal")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *AuthRequestRepo) checkDomainDiscovery(ctx context.Context, request *domain.AuthRequest, loginName string) bool {
|
func (repo *AuthRequestRepo) checkDomainDiscovery(ctx context.Context, request *domain.AuthRequest, loginName string) (bool, error) {
|
||||||
// check if there's a suffix in the loginname
|
// check if there's a suffix in the loginname
|
||||||
loginName = strings.TrimSpace(strings.ToLower(loginName))
|
loginName = strings.TrimSpace(strings.ToLower(loginName))
|
||||||
index := strings.LastIndex(loginName, "@")
|
index := strings.LastIndex(loginName, "@")
|
||||||
if index < 0 {
|
if index < 0 {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
// check if the suffix matches a verified domain
|
// check if the suffix matches a verified domain
|
||||||
org, err := repo.Query.OrgByVerifiedDomain(ctx, loginName[index+1:])
|
org, err := repo.Query.OrgByVerifiedDomain(ctx, loginName[index+1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
// and if the login policy allows domain discovery
|
// and if the login policy allows domain discovery
|
||||||
policy, err := repo.Query.LoginPolicyByID(ctx, true, org.ID, false)
|
policy, err := repo.Query.LoginPolicyByID(ctx, true, org.ID, false)
|
||||||
if err != nil || !policy.AllowDomainDiscovery {
|
if err != nil || !policy.AllowDomainDiscovery {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
// discovery was allowed, so set the org as requested org
|
// discovery was allowed, so set the org as requested org
|
||||||
// and clear all potentially existing user information and only set the loginname as hint (for registration)
|
// and clear all potentially existing user information and only set the loginname as hint (for registration)
|
||||||
|
// also ensure that the policies are read from the org
|
||||||
request.SetOrgInformation(org.ID, org.Name, org.Domain, false)
|
request.SetOrgInformation(org.ID, org.Name, org.Domain, false)
|
||||||
request.SetUserInfo("", "", "", "", "", org.ID)
|
request.SetUserInfo("", "", "", "", "", org.ID)
|
||||||
|
if err = repo.fillPolicies(ctx, request); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
request.LoginHint = loginName
|
request.LoginHint = loginName
|
||||||
request.Prompt = append(request.Prompt, domain.PromptCreate) // to trigger registration
|
request.Prompt = append(request.Prompt, domain.PromptCreate) // to trigger registration
|
||||||
return true
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *AuthRequestRepo) checkLoginNameInput(ctx context.Context, request *domain.AuthRequest, loginNameInput string) (*user_view_model.UserView, error) {
|
func (repo *AuthRequestRepo) checkLoginNameInput(ctx context.Context, request *domain.AuthRequest, loginNameInput string) (*user_view_model.UserView, error) {
|
||||||
|
@@ -3,6 +3,7 @@ package eventsourcing
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/feature"
|
||||||
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/eventstore"
|
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/spooler"
|
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/spooler"
|
||||||
auth_view "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
|
auth_view "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
|
||||||
@@ -88,6 +89,7 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, c
|
|||||||
ProjectProvider: queryView,
|
ProjectProvider: queryView,
|
||||||
ApplicationProvider: queries,
|
ApplicationProvider: queries,
|
||||||
CustomTextProvider: queries,
|
CustomTextProvider: queries,
|
||||||
|
FeatureCheck: feature.NewCheck(esV2),
|
||||||
IdGenerator: idGenerator,
|
IdGenerator: idGenerator,
|
||||||
},
|
},
|
||||||
eventstore.TokenRepo{
|
eventstore.TokenRepo{
|
||||||
|
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/id"
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
"github.com/zitadel/zitadel/internal/repository/action"
|
"github.com/zitadel/zitadel/internal/repository/action"
|
||||||
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||||
instance_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
instance_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
"github.com/zitadel/zitadel/internal/repository/keypair"
|
"github.com/zitadel/zitadel/internal/repository/keypair"
|
||||||
@@ -145,6 +146,7 @@ func StartCommands(
|
|||||||
authrequest.RegisterEventMappers(repo.eventstore)
|
authrequest.RegisterEventMappers(repo.eventstore)
|
||||||
oidcsession.RegisterEventMappers(repo.eventstore)
|
oidcsession.RegisterEventMappers(repo.eventstore)
|
||||||
milestone.RegisterEventMappers(repo.eventstore)
|
milestone.RegisterEventMappers(repo.eventstore)
|
||||||
|
feature.RegisterEventMappers(repo.eventstore)
|
||||||
|
|
||||||
repo.codeAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
|
repo.codeAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
|
||||||
repo.userPasswordHasher, err = defaults.PasswordHasher.PasswordHasher()
|
repo.userPasswordHasher, err = defaults.PasswordHasher.PasswordHasher()
|
||||||
|
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/id"
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
"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/instance"
|
||||||
"github.com/zitadel/zitadel/internal/repository/org"
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
"github.com/zitadel/zitadel/internal/repository/project"
|
"github.com/zitadel/zitadel/internal/repository/project"
|
||||||
@@ -112,6 +113,7 @@ type InstanceSetup struct {
|
|||||||
Quotas *struct {
|
Quotas *struct {
|
||||||
Items []*SetQuota
|
Items []*SetQuota
|
||||||
}
|
}
|
||||||
|
Features map[domain.Feature]any
|
||||||
}
|
}
|
||||||
|
|
||||||
type SecretGenerators struct {
|
type SecretGenerators struct {
|
||||||
@@ -432,6 +434,19 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for f, value := range setup.Features {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case bool:
|
||||||
|
wm, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, nil, err
|
||||||
|
}
|
||||||
|
validations = append(validations, prepareSetFeature(wm, feature.Boolean{Boolean: v}, c.idGenerator))
|
||||||
|
default:
|
||||||
|
return "", "", nil, nil, errors.ThrowInvalidArgument(nil, "INST-GE4tg", "Errors.Feature.TypeNotSupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
|
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, nil, err
|
return "", "", nil, nil, err
|
||||||
|
63
internal/command/instance_feature.go
Normal file
63
internal/command/instance_feature.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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/errors"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||||
|
)
|
||||||
|
|
||||||
|
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, errors.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
|
||||||
|
}
|
||||||
|
}
|
81
internal/command/instance_feature_model.go
Normal file
81
internal/command/instance_feature_model.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||||
|
)
|
||||||
|
|
||||||
|
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, errors.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 errors.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
|
||||||
|
}
|
179
internal/command/instance_feature_test.go
Normal file
179
internal/command/instance_feature_test.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
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/errors"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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: errors.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: errors.ThrowPreconditionFailed(nil, "FEAT-SDfjk", "Errors.Feature.TypeNotSupported"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"first set",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusherWithInstanceID("instanceID",
|
||||||
|
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(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusherWithInstanceID("instanceID",
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
||||||
action_repo "github.com/zitadel/zitadel/internal/repository/action"
|
action_repo "github.com/zitadel/zitadel/internal/repository/action"
|
||||||
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||||
iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
|
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
|
||||||
@@ -52,6 +53,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
|
|||||||
authrequest.RegisterEventMappers(es)
|
authrequest.RegisterEventMappers(es)
|
||||||
oidcsession.RegisterEventMappers(es)
|
oidcsession.RegisterEventMappers(es)
|
||||||
quota_repo.RegisterEventMappers(es)
|
quota_repo.RegisterEventMappers(es)
|
||||||
|
feature.RegisterEventMappers(es)
|
||||||
return es
|
return es
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
internal/config/hook/feature.go
Normal file
27
internal/config/hook/feature.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package hook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StringToFeatureHookFunc() mapstructure.DecodeHookFuncType {
|
||||||
|
return func(
|
||||||
|
f reflect.Type,
|
||||||
|
t reflect.Type,
|
||||||
|
data interface{},
|
||||||
|
) (interface{}, error) {
|
||||||
|
if f.Kind() != reflect.String {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if t != reflect.TypeOf(domain.FeatureUnspecified) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.FeatureString(data.(string))
|
||||||
|
}
|
||||||
|
}
|
@@ -208,3 +208,17 @@ func (a *AuthRequest) Done() bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AuthRequest) PrivateLabelingOrgID(defaultID string) string {
|
||||||
|
if a.RequestedOrgID != "" {
|
||||||
|
return a.RequestedOrgID
|
||||||
|
}
|
||||||
|
if (a.PrivateLabelingSetting == PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy || a.PrivateLabelingSetting == PrivateLabelingSettingUnspecified) &&
|
||||||
|
a.UserOrgID != "" {
|
||||||
|
return a.UserOrgID
|
||||||
|
}
|
||||||
|
if a.PrivateLabelingSetting != PrivateLabelingSettingUnspecified {
|
||||||
|
return a.ApplicationResourceOwner
|
||||||
|
}
|
||||||
|
return defaultID
|
||||||
|
}
|
||||||
|
28
internal/domain/feature.go
Normal file
28
internal/domain/feature.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
//go:generate enumer -type Feature
|
||||||
|
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type Feature int
|
||||||
|
|
||||||
|
func (f Feature) Type() FeatureType {
|
||||||
|
switch f {
|
||||||
|
case FeatureUnspecified:
|
||||||
|
return FeatureTypeUnspecified
|
||||||
|
case FeatureLoginDefaultOrg:
|
||||||
|
return FeatureTypeBoolean
|
||||||
|
default:
|
||||||
|
return FeatureTypeUnspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
FeatureTypeUnspecified FeatureType = iota
|
||||||
|
FeatureTypeBoolean
|
||||||
|
)
|
||||||
|
|
||||||
|
type FeatureType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FeatureUnspecified Feature = iota
|
||||||
|
FeatureLoginDefaultOrg
|
||||||
|
)
|
78
internal/domain/feature_enumer.go
Normal file
78
internal/domain/feature_enumer.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// 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
|
||||||
|
}
|
30
internal/repository/feature/aggregate.go
Normal file
30
internal/repository/feature/aggregate.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package feature
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eventTypePrefix = eventstore.EventType("feature.")
|
||||||
|
setSuffix = ".set"
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
9
internal/repository/feature/eventstore.go
Normal file
9
internal/repository/feature/eventstore.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package feature
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterEventMappers(es *eventstore.Eventstore) {
|
||||||
|
es.RegisterFilterEventMapper(AggregateType, DefaultLoginInstanceEventType, eventstore.GenericEventMapper[SetEvent[Boolean]])
|
||||||
|
}
|
65
internal/repository/feature/feature.go
Normal file
65
internal/repository/feature/feature.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package feature
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultLoginInstanceEventType = EventTypeFromFeature(domain.FeatureLoginDefaultOrg)
|
||||||
|
)
|
||||||
|
|
||||||
|
func EventTypeFromFeature(feature domain.Feature) eventstore.EventType {
|
||||||
|
return eventTypePrefix + eventstore.EventType(strings.ToLower(feature.String())) + setSuffix
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetEvent[T SetEventType] struct {
|
||||||
|
*eventstore.BaseEvent
|
||||||
|
|
||||||
|
Value T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SetEvent[T]) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SetEvent[T]) Data() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SetEvent[T]) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetEventType interface {
|
||||||
|
Boolean
|
||||||
|
FeatureType() domain.FeatureType
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventType[T SetEventType] struct {
|
||||||
|
eventstore.EventType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Boolean struct {
|
||||||
|
Boolean bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Boolean) FeatureType() domain.FeatureType {
|
||||||
|
return domain.FeatureTypeBoolean
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSetEvent[T SetEventType](
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
eventType eventstore.EventType,
|
||||||
|
setType T,
|
||||||
|
) *SetEvent[T] {
|
||||||
|
return &SetEvent[T]{
|
||||||
|
eventstore.NewBaseEventForPush(
|
||||||
|
ctx, aggregate, eventType),
|
||||||
|
setType,
|
||||||
|
}
|
||||||
|
}
|
@@ -522,6 +522,10 @@ Errors:
|
|||||||
Token:
|
Token:
|
||||||
Invalid: Токенът е невалиден
|
Invalid: Токенът е невалиден
|
||||||
Expired: Токенът е изтекъл
|
Expired: Токенът е изтекъл
|
||||||
|
Feature:
|
||||||
|
NotExisting: Функцията не съществува
|
||||||
|
TypeNotSupported: Типът функция не се поддържа
|
||||||
|
InvalidValue: Невалидна стойност за тази функция
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: Действие
|
action: Действие
|
||||||
@@ -532,6 +536,8 @@ AggregateTypes:
|
|||||||
user: Потребител
|
user: Потребител
|
||||||
usergrant: Предоставяне на потребител
|
usergrant: Предоставяне на потребител
|
||||||
quota: Квота
|
quota: Квота
|
||||||
|
feature: Особеност
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
user:
|
user:
|
||||||
added: Добавен потребител
|
added: Добавен потребител
|
||||||
|
@@ -505,6 +505,10 @@ Errors:
|
|||||||
Invalid: Token ist ungültig
|
Invalid: Token ist ungültig
|
||||||
Expired: Token ist abgelaufen
|
Expired: Token ist abgelaufen
|
||||||
InvalidClient: Token wurde nicht für diesen Client ausgestellt
|
InvalidClient: Token wurde nicht für diesen Client ausgestellt
|
||||||
|
Feature:
|
||||||
|
NotExisting: Feature existiert nicht
|
||||||
|
TypeNotSupported: Feature Typ wird nicht unterstützt
|
||||||
|
InvalidValue: Ungültiger Wert für dieses Feature
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: Action
|
action: Action
|
||||||
@@ -515,6 +519,7 @@ AggregateTypes:
|
|||||||
user: Benutzer
|
user: Benutzer
|
||||||
usergrant: Benutzerberechtigung
|
usergrant: Benutzerberechtigung
|
||||||
quota: Kontingent
|
quota: Kontingent
|
||||||
|
feature: Feature
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
user:
|
user:
|
||||||
|
@@ -505,6 +505,10 @@ Errors:
|
|||||||
Invalid: Token is invalid
|
Invalid: Token is invalid
|
||||||
Expired: Token is expired
|
Expired: Token is expired
|
||||||
InvalidClient: Token was not issued for this client
|
InvalidClient: Token was not issued for this client
|
||||||
|
Feature:
|
||||||
|
NotExisting: Feature does not exist
|
||||||
|
TypeNotSupported: Feature type is not supported
|
||||||
|
InvalidValue: Invalid value for this feature
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: Action
|
action: Action
|
||||||
@@ -515,6 +519,7 @@ AggregateTypes:
|
|||||||
user: User
|
user: User
|
||||||
usergrant: User grant
|
usergrant: User grant
|
||||||
quota: Quota
|
quota: Quota
|
||||||
|
feature: Feature
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
user:
|
user:
|
||||||
|
@@ -505,6 +505,10 @@ Errors:
|
|||||||
Invalid: El token no es válido
|
Invalid: El token no es válido
|
||||||
Expired: El token ha caducado
|
Expired: El token ha caducado
|
||||||
InvalidClient: El token no ha sido emitido para este cliente
|
InvalidClient: El token no ha sido emitido para este cliente
|
||||||
|
Feature:
|
||||||
|
NotExisting: La característica no existe
|
||||||
|
TypeNotSupported: El tipo de característica no es compatible
|
||||||
|
InvalidValue: Valor no válido para esta característica
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: Acción
|
action: Acción
|
||||||
@@ -515,6 +519,7 @@ AggregateTypes:
|
|||||||
user: Usuario
|
user: Usuario
|
||||||
usergrant: Concesión de usuario
|
usergrant: Concesión de usuario
|
||||||
quota: Cuota
|
quota: Cuota
|
||||||
|
feature: Característica
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
user:
|
user:
|
||||||
|
@@ -505,6 +505,10 @@ Errors:
|
|||||||
Invalid: Le jeton n'est pas valide
|
Invalid: Le jeton n'est pas valide
|
||||||
Expired: Le jeton est expiré
|
Expired: Le jeton est expiré
|
||||||
InvalidClient: Le token n'a pas été émis pour ce client
|
InvalidClient: Le token n'a pas été émis pour ce client
|
||||||
|
Feature:
|
||||||
|
NotExisting: La fonctionnalité n'existe pas
|
||||||
|
TypeNotSupported: Le type de fonctionnalité n'est pas pris en charge
|
||||||
|
InvalidValue: Valeur non valide pour cette fonctionnalité
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: Action
|
action: Action
|
||||||
@@ -515,6 +519,7 @@ AggregateTypes:
|
|||||||
user: Utilisateur
|
user: Utilisateur
|
||||||
usergrant: Subvention de l'utilisateur
|
usergrant: Subvention de l'utilisateur
|
||||||
quota: Contingent
|
quota: Contingent
|
||||||
|
feature: Fonctionnalité
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
user:
|
user:
|
||||||
|
@@ -505,6 +505,10 @@ Errors:
|
|||||||
Invalid: Token non è valido
|
Invalid: Token non è valido
|
||||||
Expired: Token è scaduto
|
Expired: Token è scaduto
|
||||||
InvalidClient: Il token non è stato emesso per questo cliente
|
InvalidClient: Il token non è stato emesso per questo cliente
|
||||||
|
Feature:
|
||||||
|
NotExisting: La funzionalità non esiste
|
||||||
|
TypeNotSupported: Il tipo di funzionalità non è supportato
|
||||||
|
InvalidValue: Valore non valido per questa funzionalità
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: Azione
|
action: Azione
|
||||||
@@ -515,6 +519,7 @@ AggregateTypes:
|
|||||||
user: Utente
|
user: Utente
|
||||||
usergrant: Sovvenzione utente
|
usergrant: Sovvenzione utente
|
||||||
quota: Quota
|
quota: Quota
|
||||||
|
feature: Funzionalità
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
user:
|
user:
|
||||||
|
@@ -494,6 +494,10 @@ Errors:
|
|||||||
Invalid: トークンが無効です
|
Invalid: トークンが無効です
|
||||||
Expired: トークンの有効期限が切れている
|
Expired: トークンの有効期限が切れている
|
||||||
InvalidClient: トークンが発行されていません
|
InvalidClient: トークンが発行されていません
|
||||||
|
Feature:
|
||||||
|
NotExisting: 機能が存在しません
|
||||||
|
TypeNotSupported: 機能タイプはサポートされていません
|
||||||
|
InvalidValue: この機能には無効な値です
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: アクション
|
action: アクション
|
||||||
@@ -504,6 +508,7 @@ AggregateTypes:
|
|||||||
user: ユーザー
|
user: ユーザー
|
||||||
usergrant: ユーザーグラント
|
usergrant: ユーザーグラント
|
||||||
quota: クォータ
|
quota: クォータ
|
||||||
|
feature: 特徴
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
user:
|
user:
|
||||||
|
@@ -505,6 +505,10 @@ Errors:
|
|||||||
Invalid: токенот е неважечки
|
Invalid: токенот е неважечки
|
||||||
Expired: токенот е истечен
|
Expired: токенот е истечен
|
||||||
InvalidClient: Токен не беше издаден на овој клиент
|
InvalidClient: Токен не беше издаден на овој клиент
|
||||||
|
Feature:
|
||||||
|
NotExisting: Функцијата не постои
|
||||||
|
TypeNotSupported: Типот на функција не е поддржан
|
||||||
|
InvalidValue: Неважечка вредност за оваа функција
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: Акција
|
action: Акција
|
||||||
@@ -515,6 +519,7 @@ AggregateTypes:
|
|||||||
user: Корисник
|
user: Корисник
|
||||||
usergrant: Овластување на корисник
|
usergrant: Овластување на корисник
|
||||||
quota: Квота
|
quota: Квота
|
||||||
|
feature: Карактеристика
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
user:
|
user:
|
||||||
|
@@ -505,6 +505,10 @@ Errors:
|
|||||||
Invalid: Token jest nieprawidłowy
|
Invalid: Token jest nieprawidłowy
|
||||||
Expired: Token wygasł
|
Expired: Token wygasł
|
||||||
InvalidClient: Token nie został wydany dla tego klienta
|
InvalidClient: Token nie został wydany dla tego klienta
|
||||||
|
Feature:
|
||||||
|
NotExisting: Funkcja nie istnieje
|
||||||
|
TypeNotSupported: Typ funkcji nie jest obsługiwany
|
||||||
|
InvalidValue: Nieprawidłowa wartość dla tej funkcji
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: Działanie
|
action: Działanie
|
||||||
@@ -515,6 +519,7 @@ AggregateTypes:
|
|||||||
user: Użytkownik
|
user: Użytkownik
|
||||||
usergrant: Uprawnienie użytkownika
|
usergrant: Uprawnienie użytkownika
|
||||||
quota: Limit
|
quota: Limit
|
||||||
|
feature: Funkcja
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
user:
|
user:
|
||||||
|
@@ -499,6 +499,10 @@ Errors:
|
|||||||
WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login
|
WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login
|
||||||
OIDCSession:
|
OIDCSession:
|
||||||
RefreshTokenInvalid: O Refresh Token é inválido
|
RefreshTokenInvalid: O Refresh Token é inválido
|
||||||
|
Feature:
|
||||||
|
NotExisting: O recurso não existe
|
||||||
|
TypeNotSupported: O tipo de recurso não é compatível
|
||||||
|
InvalidValue: Valor inválido para este recurso
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: Ação
|
action: Ação
|
||||||
@@ -509,6 +513,7 @@ AggregateTypes:
|
|||||||
user: Usuário
|
user: Usuário
|
||||||
usergrant: Concessão de usuário
|
usergrant: Concessão de usuário
|
||||||
quota: Cota
|
quota: Cota
|
||||||
|
feature: Recurso
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
user:
|
user:
|
||||||
|
@@ -505,6 +505,10 @@ Errors:
|
|||||||
Invalid: 令牌无效
|
Invalid: 令牌无效
|
||||||
Expired: 令牌已过期
|
Expired: 令牌已过期
|
||||||
InvalidClient: 没有为该客户发放令牌
|
InvalidClient: 没有为该客户发放令牌
|
||||||
|
Feature:
|
||||||
|
NotExisting: 功能不存在
|
||||||
|
TypeNotSupported: 不支持功能类型
|
||||||
|
InvalidValue: 此功能的值无效
|
||||||
|
|
||||||
AggregateTypes:
|
AggregateTypes:
|
||||||
action: 动作
|
action: 动作
|
||||||
@@ -515,6 +519,7 @@ AggregateTypes:
|
|||||||
user: 用户
|
user: 用户
|
||||||
usergrant: 用户授权
|
usergrant: 用户授权
|
||||||
quota: 配额
|
quota: 配额
|
||||||
|
feature: 特征
|
||||||
|
|
||||||
EventTypes:
|
EventTypes:
|
||||||
user:
|
user:
|
||||||
|
@@ -3706,6 +3706,19 @@ service AdminService {
|
|||||||
description: "Returns a list of the possible aggregate types in ZITADEL. This is used to filter the aggregate types in the list events request."
|
description: "Returns a list of the possible aggregate types in ZITADEL. This is used to filter the aggregate types in the list events request."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Activates the "LoginDefaultOrg" feature by setting the flag to "true"
|
||||||
|
// This is irreversible!
|
||||||
|
// Once activated, the login UI will use the settings of the default org (and not from the instance) if not organisation context is set
|
||||||
|
rpc ActivateFeatureLoginDefaultOrg(ActivateFeatureLoginDefaultOrgRequest) returns (ActivateFeatureLoginDefaultOrgResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
put: "/features/login_default_org"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.v1.auth_option) = {
|
||||||
|
permission: "iam.feature.write";
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -7740,3 +7753,9 @@ message ListAggregateTypesRequest {}
|
|||||||
message ListAggregateTypesResponse {
|
message ListAggregateTypesResponse {
|
||||||
repeated zitadel.event.v1.AggregateType aggregate_types = 1;
|
repeated zitadel.event.v1.AggregateType aggregate_types = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ActivateFeatureLoginDefaultOrgRequest {}
|
||||||
|
|
||||||
|
message ActivateFeatureLoginDefaultOrgResponse {
|
||||||
|
zitadel.v1.ObjectDetails details = 1;
|
||||||
|
}
|
@@ -397,6 +397,18 @@ service SystemService {
|
|||||||
permission: "authenticated";
|
permission: "authenticated";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set a feature flag on an instance
|
||||||
|
rpc SetInstanceFeature(SetInstanceFeatureRequest) returns (SetInstanceFeatureResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
put: "/instances/{instance_id}/features/{feature_id}"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.v1.auth_option) = {
|
||||||
|
permission: "authenticated";
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -879,3 +891,19 @@ message FailedEvent {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SetInstanceFeatureRequest {
|
||||||
|
string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
|
// the feature (id) according to [internal/domain/feature.go]
|
||||||
|
int32 feature_id = 2 [(validate.rules).int32 = {gt: 0}];
|
||||||
|
// value based on the feature type
|
||||||
|
oneof value {
|
||||||
|
option (validate.required) = true;
|
||||||
|
|
||||||
|
bool bool = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetInstanceFeatureResponse {
|
||||||
|
zitadel.v1.ObjectDetails details = 1;
|
||||||
|
}
|
Reference in New Issue
Block a user