mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:27:42 +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:
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/repository/action"
|
||||
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||
instance_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
@@ -145,6 +146,7 @@ func StartCommands(
|
||||
authrequest.RegisterEventMappers(repo.eventstore)
|
||||
oidcsession.RegisterEventMappers(repo.eventstore)
|
||||
milestone.RegisterEventMappers(repo.eventstore)
|
||||
feature.RegisterEventMappers(repo.eventstore)
|
||||
|
||||
repo.codeAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
|
||||
repo.userPasswordHasher, err = defaults.PasswordHasher.PasswordHasher()
|
||||
|
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"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/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
@@ -112,6 +113,7 @@ type InstanceSetup struct {
|
||||
Quotas *struct {
|
||||
Items []*SetQuota
|
||||
}
|
||||
Features map[domain.Feature]any
|
||||
}
|
||||
|
||||
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...)
|
||||
if err != nil {
|
||||
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"
|
||||
action_repo "github.com/zitadel/zitadel/internal/repository/action"
|
||||
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||
iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
||||
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)
|
||||
oidcsession.RegisterEventMappers(es)
|
||||
quota_repo.RegisterEventMappers(es)
|
||||
feature.RegisterEventMappers(es)
|
||||
return es
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user