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:
Livio Spring
2023-09-29 10:21:32 +02:00
committed by GitHub
parent d01f4d229f
commit 68bfab2fb3
41 changed files with 875 additions and 38 deletions

View File

@@ -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()

View File

@@ -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

View 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
}
}

View 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
}

View 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)
})
}
}

View File

@@ -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
}