feat: features (#1427)

* features

* features

* features

* fix json tags

* add features handler to auth

* mocks for tests

* add setup step

* fixes

* add featurelist to auth api

* grandfather state and typos

* typo

* merge new-eventstore

* fix login policy tests

* label policy in features

* audit log retention
This commit is contained in:
Livio Amstutz
2021-03-25 17:26:21 +01:00
committed by GitHub
parent c9b3839f3d
commit a4763b1e4c
97 changed files with 3335 additions and 109 deletions

View File

@@ -3,6 +3,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
"github.com/caos/zitadel/internal/config/types"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
@@ -49,13 +50,14 @@ type Commands struct {
keyAlgorithm crypto.EncryptionAlgorithm
privateKeyLifetime time.Duration
publicKeyLifetime time.Duration
tokenVerifier *authz.TokenVerifier
}
type Config struct {
Eventstore types.SQLUser
}
func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults, authZConfig authz.Config) (repo *Commands, err error) {
func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults, authZConfig authz.Config, authZRepo *authz_repo.EsRepository) (repo *Commands, err error) {
repo = &Commands{
eventstore: eventstore,
idGenerator: id.SonyFlakeGenerator,
@@ -119,6 +121,8 @@ func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults
return nil, err
}
repo.keyAlgorithm = keyAlgorithm
repo.tokenVerifier = authz.Start(authZRepo)
return repo, nil
}

View File

@@ -0,0 +1,74 @@
package command
import (
"time"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/features"
)
type FeaturesWriteModel struct {
eventstore.WriteModel
TierName string
TierDescription string
State domain.FeaturesState
StateDescription string
AuditLogRetention time.Duration
LoginPolicyFactors bool
LoginPolicyIDP bool
LoginPolicyPasswordless bool
LoginPolicyRegistration bool
LoginPolicyUsernameLogin bool
PasswordComplexityPolicy bool
LabelPolicy bool
}
func (wm *FeaturesWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *features.FeaturesSetEvent:
if e.TierName != nil {
wm.TierName = *e.TierName
}
if e.TierDescription != nil {
wm.TierDescription = *e.TierDescription
}
wm.State = domain.FeaturesStateActive
if e.State != nil {
wm.State = *e.State
}
if e.StateDescription != nil {
wm.StateDescription = *e.StateDescription
}
if e.AuditLogRetention != nil {
wm.AuditLogRetention = *e.AuditLogRetention
}
if e.LoginPolicyFactors != nil {
wm.LoginPolicyFactors = *e.LoginPolicyFactors
}
if e.LoginPolicyIDP != nil {
wm.LoginPolicyIDP = *e.LoginPolicyIDP
}
if e.LoginPolicyPasswordless != nil {
wm.LoginPolicyPasswordless = *e.LoginPolicyPasswordless
}
if e.LoginPolicyRegistration != nil {
wm.LoginPolicyRegistration = *e.LoginPolicyRegistration
}
if e.LoginPolicyUsernameLogin != nil {
wm.LoginPolicyUsernameLogin = *e.LoginPolicyUsernameLogin
}
if e.PasswordComplexityPolicy != nil {
wm.PasswordComplexityPolicy = *e.PasswordComplexityPolicy
}
if e.LabelPolicy != nil {
wm.LabelPolicy = *e.LabelPolicy
}
case *features.FeaturesRemovedEvent:
wm.State = domain.FeaturesStateRemoved
}
}
return wm.WriteModel.Reduce()
}

View File

@@ -160,3 +160,21 @@ func writeModelToIDPProvider(wm *IdentityProviderWriteModel) *domain.IDPProvider
Type: wm.IDPProviderType,
}
}
func writeModelToFeatures(wm *FeaturesWriteModel) *domain.Features {
return &domain.Features{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
TierName: wm.TierName,
TierDescription: wm.TierDescription,
State: wm.State,
StateDescription: wm.StateDescription,
AuditLogRetention: wm.AuditLogRetention,
LoginPolicyFactors: wm.LoginPolicyFactors,
LoginPolicyIDP: wm.LoginPolicyIDP,
LoginPolicyPasswordless: wm.LoginPolicyPasswordless,
LoginPolicyRegistration: wm.LoginPolicyRegistration,
LoginPolicyUsernameLogin: wm.LoginPolicyUsernameLogin,
PasswordComplexityPolicy: wm.PasswordComplexityPolicy,
LabelPolicy: wm.LabelPolicy,
}
}

View File

@@ -0,0 +1,64 @@
package command
import (
"context"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/repository/iam"
"github.com/caos/zitadel/internal/domain"
)
func (c *Commands) SetDefaultFeatures(ctx context.Context, features *domain.Features) (*domain.ObjectDetails, error) {
existingFeatures := NewIAMFeaturesWriteModel()
setEvent, err := c.setDefaultFeatures(ctx, existingFeatures, features)
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.PushEvents(ctx, setEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingFeatures, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingFeatures.WriteModel), nil
}
func (c *Commands) setDefaultFeatures(ctx context.Context, existingFeatures *IAMFeaturesWriteModel, features *domain.Features) (*iam.FeaturesSetEvent, error) {
err := c.eventstore.FilterToQueryReducer(ctx, existingFeatures)
if err != nil {
return nil, err
}
setEvent, hasChanged := existingFeatures.NewSetEvent(
ctx,
IAMAggregateFromWriteModel(&existingFeatures.FeaturesWriteModel.WriteModel),
features.TierName,
features.TierDescription,
features.State,
features.StateDescription,
features.AuditLogRetention,
features.LoginPolicyFactors,
features.LoginPolicyIDP,
features.LoginPolicyPasswordless,
features.LoginPolicyRegistration,
features.LoginPolicyUsernameLogin,
features.PasswordComplexityPolicy,
features.LabelPolicy,
)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged")
}
return setEvent, nil
}
func (c *Commands) getDefaultFeatures(ctx context.Context) (*domain.Features, error) {
existingFeatures := NewIAMFeaturesWriteModel()
err := c.eventstore.FilterToQueryReducer(ctx, existingFeatures)
if err != nil {
return nil, err
}
return writeModelToFeatures(&existingFeatures.FeaturesWriteModel), nil
}

View File

@@ -0,0 +1,115 @@
package command
import (
"context"
"time"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/features"
"github.com/caos/zitadel/internal/repository/iam"
)
type IAMFeaturesWriteModel struct {
FeaturesWriteModel
}
func NewIAMFeaturesWriteModel() *IAMFeaturesWriteModel {
return &IAMFeaturesWriteModel{
FeaturesWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: domain.IAMID,
ResourceOwner: domain.IAMID,
},
},
}
}
func (wm *IAMFeaturesWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *iam.FeaturesSetEvent:
wm.FeaturesWriteModel.AppendEvents(&e.FeaturesSetEvent)
}
}
}
func (wm *IAMFeaturesWriteModel) IsValid() bool {
return wm.AggregateID != ""
}
func (wm *IAMFeaturesWriteModel) Reduce() error {
return wm.FeaturesWriteModel.Reduce()
}
func (wm *IAMFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, iam.AggregateType).
AggregateIDs(wm.FeaturesWriteModel.AggregateID).
ResourceOwner(wm.ResourceOwner).
EventTypes(iam.FeaturesSetEventType)
}
func (wm *IAMFeaturesWriteModel) NewSetEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
tierName, tierDescription string,
state domain.FeaturesState,
stateDescription string,
auditLogRetention time.Duration,
loginPolicyFactors,
loginPolicyIDP,
loginPolicyPasswordless,
loginPolicyRegistration,
loginPolicyUsernameLogin,
passwordComplexityPolicy,
labelPolicy bool,
) (*iam.FeaturesSetEvent, bool) {
changes := make([]features.FeaturesChanges, 0)
if tierName != "" && wm.TierName != tierName {
changes = append(changes, features.ChangeTierName(tierName))
}
if tierDescription != "" && wm.TierDescription != tierDescription {
changes = append(changes, features.ChangeTierDescription(tierDescription))
}
if wm.State != state {
changes = append(changes, features.ChangeState(state))
}
if stateDescription != "" && wm.StateDescription != stateDescription {
changes = append(changes, features.ChangeStateDescription(stateDescription))
}
if auditLogRetention != 0 && wm.AuditLogRetention != auditLogRetention {
changes = append(changes, features.ChangeAuditLogRetention(auditLogRetention))
}
if wm.LoginPolicyFactors != loginPolicyFactors {
changes = append(changes, features.ChangeLoginPolicyFactors(loginPolicyFactors))
}
if wm.LoginPolicyIDP != loginPolicyIDP {
changes = append(changes, features.ChangeLoginPolicyIDP(loginPolicyIDP))
}
if wm.LoginPolicyPasswordless != loginPolicyPasswordless {
changes = append(changes, features.ChangeLoginPolicyPasswordless(loginPolicyPasswordless))
}
if wm.LoginPolicyRegistration != loginPolicyRegistration {
changes = append(changes, features.ChangeLoginPolicyRegistration(loginPolicyRegistration))
}
if wm.LoginPolicyUsernameLogin != loginPolicyUsernameLogin {
changes = append(changes, features.ChangeLoginPolicyUsernameLogin(loginPolicyUsernameLogin))
}
if wm.PasswordComplexityPolicy != passwordComplexityPolicy {
changes = append(changes, features.ChangePasswordComplexityPolicy(passwordComplexityPolicy))
}
if wm.LabelPolicy != labelPolicy {
changes = append(changes, features.ChangeLabelPolicy(labelPolicy))
}
if len(changes) == 0 {
return nil, false
}
changedEvent, err := iam.NewFeaturesSetEvent(ctx, aggregate, changes)
if err != nil {
return nil, false
}
return changedEvent, true
}

View File

@@ -221,7 +221,7 @@ func (c *Commands) AddMultiFactorToDefaultLoginPolicy(ctx context.Context, multi
return domain.MultiFactorTypeUnspecified, nil, caos_errs.ThrowInvalidArgument(nil, "IAM-5m9fs", "Errors.IAM.LoginPolicy.MFA.Unspecified")
}
multiFactorModel := NewIAMMultiFactorWriteModel(multiFactor)
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
event, err := c.addMultiFactorToDefaultLoginPolicy(ctx, iamAgg, multiFactorModel, multiFactor)
if err != nil {
return domain.MultiFactorTypeUnspecified, nil, err
@@ -235,7 +235,7 @@ func (c *Commands) AddMultiFactorToDefaultLoginPolicy(ctx context.Context, multi
if err != nil {
return domain.MultiFactorTypeUnspecified, nil, err
}
return multiFactorModel.MultiFactoryWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
return multiFactorModel.MultiFactorWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
}
func (c *Commands) addMultiFactorToDefaultLoginPolicy(ctx context.Context, iamAgg *eventstore.Aggregate, multiFactorModel *IAMMultiFactorWriteModel, multiFactor domain.MultiFactorType) (eventstore.EventPusher, error) {
@@ -262,7 +262,7 @@ func (c *Commands) RemoveMultiFactorFromDefaultLoginPolicy(ctx context.Context,
if multiFactorModel.State == domain.FactorStateUnspecified || multiFactorModel.State == domain.FactorStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "IAM-3M9df", "Errors.IAM.LoginPolicy.MFA.NotExisting")
}
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
pushedEvents, err := c.eventstore.PushEvents(ctx, iam_repo.NewLoginPolicyMultiFactorRemovedEvent(ctx, iamAgg, multiFactor))
if err != nil {
return nil, err

View File

@@ -51,12 +51,12 @@ func (wm *IAMSecondFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
}
type IAMMultiFactorWriteModel struct {
MultiFactoryWriteModel
MultiFactorWriteModel
}
func NewIAMMultiFactorWriteModel(factorType domain.MultiFactorType) *IAMMultiFactorWriteModel {
return &IAMMultiFactorWriteModel{
MultiFactoryWriteModel{
MultiFactorWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: domain.IAMID,
ResourceOwner: domain.IAMID,
@@ -82,7 +82,7 @@ func (wm *IAMMultiFactorWriteModel) AppendEvents(events ...eventstore.EventReade
}
func (wm *IAMMultiFactorWriteModel) Reduce() error {
return wm.MultiFactoryWriteModel.Reduce()
return wm.MultiFactorWriteModel.Reduce()
}
func (wm *IAMMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder {

View File

@@ -2,7 +2,14 @@ package command
import (
"context"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/repository/mock"
@@ -12,9 +19,6 @@ import (
proj_repo "github.com/caos/zitadel/internal/repository/project"
usr_repo "github.com/caos/zitadel/internal/repository/user"
"github.com/caos/zitadel/internal/repository/usergrant"
"github.com/golang/mock/gomock"
"testing"
"time"
)
type expect func(mockRepository *mock.MockRepository)
@@ -172,3 +176,48 @@ func GetMockSecretGenerator(t *testing.T) crypto.Generator {
return generator
}
func GetMockVerifier(t *testing.T, features ...string) *authz.TokenVerifier {
return authz.Start(&testVerifier{
features: features,
})
}
type testVerifier struct {
features []string
}
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID string) (string, string, string, string, error) {
return "userID", "agentID", "de", "orgID", nil
}
func (v *testVerifier) SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error) {
return nil, nil
}
func (v *testVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
return "", nil, nil
}
func (v *testVerifier) ExistsOrg(ctx context.Context, orgID string) error {
return nil
}
func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, error) {
return "clientID", nil
}
func (v *testVerifier) CheckOrgFeatures(ctx context.Context, orgID string, requiredFeatures ...string) error {
for _, feature := range requiredFeatures {
hasFeature := false
for _, f := range v.features {
if f == feature {
hasFeature = true
break
}
}
if !hasFeature {
return errors.ThrowPermissionDenied(nil, "id", "missing feature")
}
}
return nil
}

View File

@@ -0,0 +1,67 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/repository/org"
)
func (c *Commands) SetOrgFeatures(ctx context.Context, resourceOwner string, features *domain.Features) (*domain.ObjectDetails, error) {
existingFeatures := NewOrgFeaturesWriteModel(resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, existingFeatures)
if err != nil {
return nil, err
}
setEvent, hasChanged := existingFeatures.NewSetEvent(
ctx,
OrgAggregateFromWriteModel(&existingFeatures.FeaturesWriteModel.WriteModel),
features.TierName,
features.TierDescription,
features.State,
features.StateDescription,
features.AuditLogRetention,
features.LoginPolicyFactors,
features.LoginPolicyIDP,
features.LoginPolicyPasswordless,
features.LoginPolicyRegistration,
features.LoginPolicyUsernameLogin,
features.PasswordComplexityPolicy,
features.LabelPolicy,
)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged")
}
pushedEvents, err := c.eventstore.PushEvents(ctx, setEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingFeatures, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingFeatures.WriteModel), nil
}
func (c *Commands) RemoveOrgFeatures(ctx context.Context, orgID string) (*domain.ObjectDetails, error) {
existingFeatures := NewOrgFeaturesWriteModel(orgID)
err := c.eventstore.FilterToQueryReducer(ctx, existingFeatures)
if err != nil {
return nil, err
}
if existingFeatures.State == domain.FeaturesStateUnspecified || existingFeatures.State == domain.FeaturesStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "Features-Bg32G", "Errors.Features.NotFound")
}
orgAgg := OrgAggregateFromWriteModel(&existingFeatures.FeaturesWriteModel.WriteModel)
pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewFeaturesRemovedEvent(ctx, orgAgg))
if err != nil {
return nil, err
}
err = AppendAndReduce(existingFeatures, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingFeatures.WriteModel), nil
}

View File

@@ -0,0 +1,121 @@
package command
import (
"context"
"time"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/features"
"github.com/caos/zitadel/internal/repository/org"
)
type OrgFeaturesWriteModel struct {
FeaturesWriteModel
}
func NewOrgFeaturesWriteModel(orgID string) *OrgFeaturesWriteModel {
return &OrgFeaturesWriteModel{
FeaturesWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: orgID,
ResourceOwner: orgID,
},
},
}
}
func (wm *OrgFeaturesWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *org.FeaturesSetEvent:
wm.FeaturesWriteModel.AppendEvents(&e.FeaturesSetEvent)
case *org.FeaturesRemovedEvent:
wm.FeaturesWriteModel.AppendEvents(&e.FeaturesRemovedEvent)
}
}
}
func (wm *OrgFeaturesWriteModel) IsValid() bool {
return wm.AggregateID != ""
}
func (wm *OrgFeaturesWriteModel) Reduce() error {
return wm.FeaturesWriteModel.Reduce()
}
func (wm *OrgFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, org.AggregateType).
AggregateIDs(wm.FeaturesWriteModel.AggregateID).
ResourceOwner(wm.ResourceOwner).
EventTypes(
org.FeaturesSetEventType,
org.FeaturesRemovedEventType,
)
}
func (wm *OrgFeaturesWriteModel) NewSetEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
tierName,
tierDescription string,
state domain.FeaturesState,
stateDescription string,
auditLogRetention time.Duration,
loginPolicyFactors,
loginPolicyIDP,
loginPolicyPasswordless,
loginPolicyRegistration,
loginPolicyUsernameLogin,
passwordComplexityPolicy,
labelPolicy bool,
) (*org.FeaturesSetEvent, bool) {
changes := make([]features.FeaturesChanges, 0)
if tierName != "" && wm.TierName != tierName {
changes = append(changes, features.ChangeTierName(tierName))
}
if tierDescription != "" && wm.TierDescription != tierDescription {
changes = append(changes, features.ChangeTierDescription(tierDescription))
}
if wm.State != state {
changes = append(changes, features.ChangeState(state))
}
if stateDescription != "" && wm.StateDescription != stateDescription {
changes = append(changes, features.ChangeStateDescription(stateDescription))
}
if auditLogRetention != 0 && wm.AuditLogRetention != auditLogRetention {
changes = append(changes, features.ChangeAuditLogRetention(auditLogRetention))
}
if wm.LoginPolicyFactors != loginPolicyFactors {
changes = append(changes, features.ChangeLoginPolicyFactors(loginPolicyFactors))
}
if wm.LoginPolicyIDP != loginPolicyIDP {
changes = append(changes, features.ChangeLoginPolicyIDP(loginPolicyIDP))
}
if wm.LoginPolicyPasswordless != loginPolicyPasswordless {
changes = append(changes, features.ChangeLoginPolicyPasswordless(loginPolicyPasswordless))
}
if wm.LoginPolicyRegistration != loginPolicyRegistration {
changes = append(changes, features.ChangeLoginPolicyRegistration(loginPolicyRegistration))
}
if wm.LoginPolicyUsernameLogin != loginPolicyUsernameLogin {
changes = append(changes, features.ChangeLoginPolicyUsernameLogin(loginPolicyUsernameLogin))
}
if wm.PasswordComplexityPolicy != passwordComplexityPolicy {
changes = append(changes, features.ChangePasswordComplexityPolicy(passwordComplexityPolicy))
}
if wm.LabelPolicy != labelPolicy {
changes = append(changes, features.ChangeLabelPolicy(labelPolicy))
}
if len(changes) == 0 {
return nil, false
}
changedEvent, err := org.NewFeaturesSetEvent(ctx, aggregate, changes)
if err != nil {
return nil, false
}
return changedEvent, true
}

View File

@@ -2,7 +2,11 @@ package command
import (
"context"
"reflect"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
@@ -22,6 +26,11 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
return nil, caos_errs.ThrowAlreadyExists(nil, "Org-Dgfb2", "Errors.Org.LoginPolicy.AlreadyExists")
}
err = c.checkLoginPolicyAllowed(ctx, resourceOwner, policy)
if err != nil {
return nil, err
}
orgAgg := OrgAggregateFromWriteModel(&addedPolicy.WriteModel)
pushedEvents, err := c.eventstore.PushEvents(
ctx,
@@ -43,6 +52,15 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
return writeModelToLoginPolicy(&addedPolicy.LoginPolicyWriteModel), nil
}
func (c *Commands) orgLoginPolicyWriteModelByID(ctx context.Context, orgID string) (*OrgLoginPolicyWriteModel, error) {
policyWriteModel := NewOrgLoginPolicyWriteModel(orgID)
err := c.eventstore.FilterToQueryReducer(ctx, policyWriteModel)
if err != nil {
return nil, err
}
return policyWriteModel, nil
}
func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string, policy *domain.LoginPolicy) (*domain.LoginPolicy, error) {
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Mf9sf", "Errors.ResourceOwnerMissing")
@@ -55,6 +73,12 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string,
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "Org-M0sif", "Errors.Org.LoginPolicy.NotFound")
}
err = c.checkLoginPolicyAllowed(ctx, resourceOwner, policy)
if err != nil {
return nil, err
}
orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LoginPolicyWriteModel.WriteModel)
changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, orgAgg, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType)
if !hasChanged {
@@ -72,6 +96,30 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string,
return writeModelToLoginPolicy(&existingPolicy.LoginPolicyWriteModel), nil
}
func (c *Commands) checkLoginPolicyAllowed(ctx context.Context, resourceOwner string, policy *domain.LoginPolicy) error {
defaultPolicy, err := c.getDefaultLoginPolicy(ctx)
if err != nil {
return err
}
requiredFeatures := make([]string, 0)
if defaultPolicy.ForceMFA != policy.ForceMFA || !reflect.DeepEqual(defaultPolicy.MultiFactors, policy.MultiFactors) || !reflect.DeepEqual(defaultPolicy.SecondFactors, policy.SecondFactors) {
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyFactors)
}
if defaultPolicy.AllowExternalIDP != policy.AllowExternalIDP || !reflect.DeepEqual(defaultPolicy.IDPProviders, policy.IDPProviders) {
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyIDP)
}
if defaultPolicy.AllowRegister != policy.AllowRegister {
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyRegistration)
}
if defaultPolicy.PasswordlessType != policy.PasswordlessType {
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyPasswordless)
}
if defaultPolicy.AllowUsernamePassword != policy.AllowUsernamePassword {
requiredFeatures = append(requiredFeatures, domain.FeatureLoginPolicyUsernameLogin)
}
return authz.CheckOrgFeatures(ctx, c.tokenVerifier, resourceOwner, requiredFeatures...)
}
func (c *Commands) RemoveLoginPolicy(ctx context.Context, orgID string) (*domain.ObjectDetails, error) {
if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-55Mg9", "Errors.ResourceOwnerMissing")
@@ -267,7 +315,7 @@ func (c *Commands) AddMultiFactorToLoginPolicy(ctx context.Context, multiFactor
if err != nil {
return domain.MultiFactorTypeUnspecified, nil, err
}
return multiFactorModel.MultiFactoryWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
return multiFactorModel.MultiFactorWriteModel.MFAType, writeModelToObjectDetails(&multiFactorModel.WriteModel), nil
}
func (c *Commands) RemoveMultiFactorFromLoginPolicy(ctx context.Context, multiFactor domain.MultiFactorType, orgID string) (*domain.ObjectDetails, error) {
@@ -285,7 +333,7 @@ func (c *Commands) RemoveMultiFactorFromLoginPolicy(ctx context.Context, multiFa
if multiFactorModel.State == domain.FactorStateUnspecified || multiFactorModel.State == domain.FactorStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "Org-3M9df", "Errors.Org.LoginPolicy.MFA.NotExisting")
}
orgAgg := OrgAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
orgAgg := OrgAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewLoginPolicyMultiFactorRemovedEvent(ctx, orgAgg, multiFactor))
if err != nil {

View File

@@ -51,12 +51,12 @@ func (wm *OrgSecondFactorWriteModel) Query() *eventstore.SearchQueryBuilder {
}
type OrgMultiFactorWriteModel struct {
MultiFactoryWriteModel
MultiFactorWriteModel
}
func NewOrgMultiFactorWriteModel(orgID string, factorType domain.MultiFactorType) *OrgMultiFactorWriteModel {
return &OrgMultiFactorWriteModel{
MultiFactoryWriteModel{
MultiFactorWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: orgID,
ResourceOwner: orgID,
@@ -82,7 +82,7 @@ func (wm *OrgMultiFactorWriteModel) AppendEvents(events ...eventstore.EventReade
}
func (wm *OrgMultiFactorWriteModel) Reduce() error {
return wm.MultiFactoryWriteModel.Reduce()
return wm.MultiFactorWriteModel.Reduce()
}
func (wm *OrgMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder {

View File

@@ -6,11 +6,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/repository/iam"
"github.com/caos/zitadel/internal/repository/org"
"github.com/caos/zitadel/internal/repository/policy"
"github.com/caos/zitadel/internal/repository/user"
@@ -18,7 +20,8 @@ import (
func TestCommandSide_AddLoginPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore *eventstore.Eventstore
tokenVerifier *authz.TokenVerifier
}
type args struct {
ctx context.Context
@@ -88,12 +91,60 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
err: caos_errs.IsErrorAlreadyExists,
},
},
{
name: "loginpolicy not allowed, permission denied error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
false,
true,
true,
true,
domain.PasswordlessTypeAllowed,
),
),
),
),
tokenVerifier: GetMockVerifier(t),
},
args: args{
ctx: context.Background(),
orgID: "org1",
policy: &domain.LoginPolicy{
AllowRegister: true,
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
},
},
res: res{
err: caos_errs.IsPermissionDenied,
},
},
{
name: "add policy,ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
false,
true,
true,
true,
domain.PasswordlessTypeAllowed,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -109,6 +160,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
},
),
),
tokenVerifier: GetMockVerifier(t, domain.FeatureLoginPolicyUsernameLogin),
},
args: args{
ctx: context.Background(),
@@ -139,7 +191,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore,
tokenVerifier: tt.fields.tokenVerifier,
}
got, err := r.AddLoginPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy)
if tt.res.err == nil {
@@ -157,7 +210,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore *eventstore.Eventstore
tokenVerifier *authz.TokenVerifier
}
type args struct {
ctx context.Context
@@ -218,6 +272,53 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
err: caos_errs.IsNotFound,
},
},
{
name: "not allowed, permission denied error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewLoginPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
true,
true,
true,
true,
domain.PasswordlessTypeAllowed,
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
false,
true,
true,
true,
domain.PasswordlessTypeAllowed,
),
),
),
),
tokenVerifier: GetMockVerifier(t),
},
args: args{
ctx: context.Background(),
orgID: "org1",
policy: &domain.LoginPolicy{
AllowRegister: true,
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
},
},
res: res{
err: caos_errs.IsPermissionDenied,
},
},
{
name: "no changes, precondition error",
fields: fields{
@@ -235,7 +336,20 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
false,
true,
true,
true,
domain.PasswordlessTypeAllowed,
),
),
),
),
tokenVerifier: GetMockVerifier(t, domain.FeatureLoginPolicyUsernameLogin),
},
args: args{
ctx: context.Background(),
@@ -269,6 +383,18 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
iam.NewLoginPolicyAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
false,
false,
false,
false,
domain.PasswordlessTypeNotAllowed,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -277,6 +403,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
},
),
),
tokenVerifier: GetMockVerifier(t, domain.FeatureLoginPolicyUsernameLogin),
},
args: args{
ctx: context.Background(),
@@ -307,7 +434,8 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore,
tokenVerifier: tt.fields.tokenVerifier,
}
got, err := r.ChangeLoginPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy)
if tt.res.err == nil {

View File

@@ -26,13 +26,13 @@ func (wm *SecondFactorWriteModel) Reduce() error {
return wm.WriteModel.Reduce()
}
type MultiFactoryWriteModel struct {
type MultiFactorWriteModel struct {
eventstore.WriteModel
MFAType domain.MultiFactorType
State domain.FactorState
}
func (wm *MultiFactoryWriteModel) Reduce() error {
func (wm *MultiFactorWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *policy.MultiFactorAddedEvent:

View File

@@ -0,0 +1,52 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/config/types"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
)
type Step12 struct {
TierName string
TierDescription string
AuditLogRetention types.Duration
LoginPolicyFactors bool
LoginPolicyIDP bool
LoginPolicyPasswordless bool
LoginPolicyRegistration bool
LoginPolicyUsernameLogin bool
PasswordComplexityPolicy bool
}
func (s *Step12) Step() domain.Step {
return domain.Step12
}
func (s *Step12) execute(ctx context.Context, commandSide *Commands) error {
return commandSide.SetupStep12(ctx, s)
}
func (c *Commands) SetupStep12(ctx context.Context, step *Step12) error {
fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) {
featuresWriteModel := NewIAMFeaturesWriteModel()
featuresEvent, err := c.setDefaultFeatures(ctx, featuresWriteModel, &domain.Features{
TierName: step.TierName,
TierDescription: step.TierDescription,
State: domain.FeaturesStateActive,
AuditLogRetention: step.AuditLogRetention.Duration,
LoginPolicyFactors: step.LoginPolicyFactors,
LoginPolicyIDP: step.LoginPolicyIDP,
LoginPolicyPasswordless: step.LoginPolicyPasswordless,
LoginPolicyRegistration: step.LoginPolicyRegistration,
LoginPolicyUsernameLogin: step.LoginPolicyUsernameLogin,
PasswordComplexityPolicy: step.PasswordComplexityPolicy,
})
if err != nil {
return nil, err
}
return []eventstore.EventPusher{featuresEvent}, nil
}
return c.setup(ctx, step, fn)
}

View File

@@ -23,7 +23,7 @@ func (s *Step9) execute(ctx context.Context, commandSide *Commands) error {
func (c *Commands) SetupStep9(ctx context.Context, step *Step9) error {
fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) {
multiFactorModel := NewIAMMultiFactorWriteModel(domain.MultiFactorTypeU2FWithPIN)
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel)
iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactorWriteModel.WriteModel)
if !step.Passwordless {
return []eventstore.EventPusher{}, nil
}