feat: option to disallow public org registration (#6917)

* feat: return 404 or 409 if org reg disallowed

* fix: system limit permissions

* feat: add iam limits api

* feat: disallow public org registrations on default instance

* add integration test

* test: integration

* fix test

* docs: describe public org registrations

* avoid updating docs deps

* fix system limits integration test

* silence integration tests

* fix linting

* ignore strange linter complaints

* review

* improve reset properties naming

* redefine the api

* use restrictions aggregate

* test query

* simplify and test projection

* test commands

* fix unit tests

* move integration test

* support restrictions on default instance

* also test GetRestrictions

* self review

* lint

* abstract away resource owner

* fix tests

* lint
This commit is contained in:
Elio Bischof
2023-11-22 10:29:38 +01:00
committed by GitHub
parent 5fa596a871
commit 76fe032b5f
45 changed files with 1280 additions and 123 deletions

View File

@@ -32,6 +32,7 @@ import (
"github.com/zitadel/zitadel/internal/repository/org"
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/quota"
"github.com/zitadel/zitadel/internal/repository/restrictions"
"github.com/zitadel/zitadel/internal/repository/session"
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
usr_grant_repo "github.com/zitadel/zitadel/internal/repository/usergrant"
@@ -152,6 +153,7 @@ func StartCommands(
action.RegisterEventMappers(repo.eventstore)
quota.RegisterEventMappers(repo.eventstore)
limits.RegisterEventMappers(repo.eventstore)
restrictions.RegisterEventMappers(repo.eventstore)
session.RegisterEventMappers(repo.eventstore)
idpintent.RegisterEventMappers(repo.eventstore)
authrequest.RegisterEventMappers(repo.eventstore)

View File

@@ -21,6 +21,7 @@ import (
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/quota"
"github.com/zitadel/zitadel/internal/repository/restrictions"
"github.com/zitadel/zitadel/internal/repository/user"
)
@@ -115,10 +116,9 @@ type InstanceSetup struct {
Quotas *struct {
Items []*SetQuota
}
Features map[domain.Feature]any
Limits *struct {
AuditLogRetention *time.Duration
}
Features map[domain.Feature]any
Limits *SetLimits
Restrictions *SetRestrictions
}
type SecretGenerators struct {
@@ -135,12 +135,13 @@ type SecretGenerators struct {
}
type ZitadelConfig struct {
projectID string
mgmtAppID string
adminAppID string
authAppID string
consoleAppID string
limitsID string
projectID string
mgmtAppID string
adminAppID string
authAppID string
consoleAppID string
limitsID string
restrictionsID string
}
func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) {
@@ -169,6 +170,10 @@ func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) {
return err
}
s.zitadel.limitsID, err = idGenerator.Next()
if err != nil {
return err
}
s.zitadel.restrictionsID, err = idGenerator.Next()
return err
}
@@ -200,6 +205,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
userAgg := user.NewAggregate(userID, orgID)
projectAgg := project.NewAggregate(setup.zitadel.projectID, orgID)
limitsAgg := limits.NewAggregate(setup.zitadel.limitsID, instanceID, instanceID)
restrictionsAgg := restrictions.NewAggregate(setup.zitadel.restrictionsID, instanceID, instanceID)
validations := []preparation.Validation{
prepareAddInstance(instanceAgg, setup.InstanceName, setup.DefaultLanguage),
@@ -453,9 +459,11 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
}
if setup.Limits != nil {
validations = append(validations, c.SetLimitsCommand(limitsAgg, &limitsWriteModel{}, &SetLimits{
AuditLogRetention: setup.Limits.AuditLogRetention,
}))
validations = append(validations, c.SetLimitsCommand(limitsAgg, &limitsWriteModel{}, setup.Limits))
}
if setup.Restrictions != nil {
validations = append(validations, c.SetRestrictionsCommand(restrictionsAgg, &restrictionsWriteModel{}, setup.Restrictions))
}
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)

View File

@@ -13,7 +13,7 @@ import (
)
type SetLimits struct {
AuditLogRetention *time.Duration `json:"AuditLogRetention,omitempty"`
AuditLogRetention *time.Duration
}
// SetLimits creates new limits or updates existing limits.
@@ -34,14 +34,14 @@ func (c *Commands) SetLimits(
return nil, err
}
}
if err != nil {
return nil, err
}
createCmds, err := c.SetLimitsCommand(limits.NewAggregate(aggregateId, instanceId, resourceOwner), wm, setLimits)()
if err != nil {
return nil, err
}
cmds, err := createCmds(ctx, nil)
if err != nil {
return nil, err
}
if len(cmds) > 0 {
events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {

View File

@@ -29,6 +29,7 @@ import (
"github.com/zitadel/zitadel/internal/repository/org"
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
quota_repo "github.com/zitadel/zitadel/internal/repository/quota"
"github.com/zitadel/zitadel/internal/repository/restrictions"
"github.com/zitadel/zitadel/internal/repository/session"
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/repository/usergrant"
@@ -60,6 +61,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
oidcsession.RegisterEventMappers(es)
quota_repo.RegisterEventMappers(es)
limits.RegisterEventMappers(es)
restrictions.RegisterEventMappers(es)
feature.RegisterEventMappers(es)
return es
}

View File

@@ -0,0 +1,81 @@
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/repository/restrictions"
)
type SetRestrictions struct {
DisallowPublicOrgRegistration *bool
}
// SetRestrictions creates new restrictions or updates existing restrictions.
func (c *Commands) SetInstanceRestrictions(
ctx context.Context,
setRestrictions *SetRestrictions,
) (*domain.ObjectDetails, error) {
instanceId := authz.GetInstance(ctx).InstanceID()
wm, err := c.getRestrictionsWriteModel(ctx, instanceId, instanceId)
if err != nil {
return nil, err
}
aggregateId := wm.AggregateID
if aggregateId == "" {
aggregateId, err = c.idGenerator.Next()
if err != nil {
return nil, err
}
}
setCmd, err := c.SetRestrictionsCommand(restrictions.NewAggregate(aggregateId, instanceId, instanceId), wm, setRestrictions)()
if err != nil {
return nil, err
}
cmds, err := setCmd(ctx, nil)
if err != nil {
return nil, err
}
if len(cmds) > 0 {
events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
err = AppendAndReduce(wm, events...)
if err != nil {
return nil, err
}
}
return writeModelToObjectDetails(&wm.WriteModel), nil
}
func (c *Commands) getRestrictionsWriteModel(ctx context.Context, instanceId, resourceOwner string) (*restrictionsWriteModel, error) {
wm := newRestrictionsWriteModel(instanceId, resourceOwner)
return wm, c.eventstore.FilterToQueryReducer(ctx, wm)
}
func (c *Commands) SetRestrictionsCommand(a *restrictions.Aggregate, wm *restrictionsWriteModel, setRestrictions *SetRestrictions) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if setRestrictions == nil || setRestrictions.DisallowPublicOrgRegistration == nil {
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-oASwj", "Errors.Restrictions.NoneSpecified")
}
return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
changes := wm.NewChanges(setRestrictions)
if len(changes) == 0 {
return nil, nil
}
return []eventstore.Command{restrictions.NewSetEvent(
eventstore.NewBaseEventForPush(
ctx,
&a.Aggregate,
restrictions.SetEventType,
),
changes...,
)}, nil
}, nil
}
}

View File

@@ -0,0 +1,55 @@
package command
import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/restrictions"
)
type restrictionsWriteModel struct {
eventstore.WriteModel
disallowPublicOrgRegistrations bool
}
// newRestrictionsWriteModel aggregateId is filled by reducing unit matching events
func newRestrictionsWriteModel(instanceId, resourceOwner string) *restrictionsWriteModel {
return &restrictionsWriteModel{
WriteModel: eventstore.WriteModel{
InstanceID: instanceId,
ResourceOwner: resourceOwner,
},
}
}
func (wm *restrictionsWriteModel) Query() *eventstore.SearchQueryBuilder {
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
InstanceID(wm.InstanceID).
AddQuery().
AggregateTypes(restrictions.AggregateType).
EventTypes(restrictions.SetEventType)
return query.Builder()
}
func (wm *restrictionsWriteModel) Reduce() error {
for _, event := range wm.Events {
wm.ChangeDate = event.CreatedAt()
if e, ok := event.(*restrictions.SetEvent); ok && e.DisallowPublicOrgRegistrations != nil {
wm.disallowPublicOrgRegistrations = *e.DisallowPublicOrgRegistrations
}
}
return wm.WriteModel.Reduce()
}
// NewChanges returns all changes that need to be applied to the aggregate.
// nil properties in setRestrictions are ignored
func (wm *restrictionsWriteModel) NewChanges(setRestrictions *SetRestrictions) (changes []restrictions.RestrictionsChange) {
if setRestrictions == nil {
return nil
}
changes = make([]restrictions.RestrictionsChange, 0, 1)
if setRestrictions.DisallowPublicOrgRegistration != nil && (wm.disallowPublicOrgRegistrations != *setRestrictions.DisallowPublicOrgRegistration) {
changes = append(changes, restrictions.ChangePublicOrgRegistrations(*setRestrictions.DisallowPublicOrgRegistration))
}
return changes
}

View File

@@ -0,0 +1,189 @@
package command
import (
"context"
"testing"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
zitadel_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
id_mock "github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/restrictions"
)
func TestSetRestrictions(t *testing.T) {
type fields func(*testing.T) (*eventstore.Eventstore, id.Generator)
type args struct {
ctx context.Context
setRestrictions *SetRestrictions
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "set new restrictions",
fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) {
return eventstoreExpect(
t,
expectFilter(),
expectPush(
eventFromEventPusherWithInstanceID(
"instance1",
restrictions.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
restrictions.SetEventType,
),
restrictions.ChangePublicOrgRegistrations(true),
),
),
),
),
id_mock.NewIDGeneratorExpectIDs(t, "restrictions1")
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
setRestrictions: &SetRestrictions{
DisallowPublicOrgRegistration: gu.Ptr(true),
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "instance1",
},
},
},
{
name: "change restrictions",
fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) {
return eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
restrictions.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
restrictions.SetEventType,
),
restrictions.ChangePublicOrgRegistrations(true),
),
),
),
expectPush(
eventFromEventPusherWithInstanceID(
"instance1",
restrictions.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
restrictions.SetEventType,
),
restrictions.ChangePublicOrgRegistrations(false),
),
),
),
),
nil
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
setRestrictions: &SetRestrictions{
DisallowPublicOrgRegistration: gu.Ptr(false),
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "instance1",
},
},
},
{
name: "set restrictions idempotency",
fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) {
return eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
restrictions.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
restrictions.SetEventType,
),
restrictions.ChangePublicOrgRegistrations(true),
),
),
),
),
nil
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
setRestrictions: &SetRestrictions{
DisallowPublicOrgRegistration: gu.Ptr(true),
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "instance1",
},
},
},
{
name: "no restrictions defined",
fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) {
return eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
restrictions.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate,
restrictions.SetEventType,
),
restrictions.ChangePublicOrgRegistrations(true),
),
),
),
), nil
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
setRestrictions: &SetRestrictions{},
},
res: res{
err: zitadel_errs.IsErrorInvalidArgument,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := new(Commands)
r.eventstore, r.idGenerator = tt.fields(t)
got, err := r.SetInstanceRestrictions(tt.args.ctx, tt.args.setRestrictions)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}