+
{{
(componentType === LoginMethodComponentType.SecondFactor
diff --git a/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.ts b/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.ts
index 8af41d6556..e16f722a63 100644
--- a/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.ts
+++ b/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { TranslateService } from '@ngx-translate/core';
@@ -34,14 +34,15 @@ export enum LoginMethodComponentType {
templateUrl: './factor-table.component.html',
styleUrls: ['./factor-table.component.scss'],
})
-export class FactorTableComponent implements OnInit {
+export class FactorTableComponent {
public LoginMethodComponentType: any = LoginMethodComponentType;
@Input() componentType!: LoginMethodComponentType;
@Input() public serviceType!: PolicyComponentServiceType;
@Input() service!: AdminService | ManagementService;
@Input() disabled: boolean = false;
+ @Input() list: Array = [];
+ @Output() listChanged: EventEmitter = new EventEmitter();
@ViewChild(MatPaginator) public paginator!: MatPaginator;
- public mfas: Array = [];
private loadingSubject: BehaviorSubject = new BehaviorSubject(false);
public loading$: Observable = this.loadingSubject.asObservable();
@@ -50,10 +51,6 @@ export class FactorTableComponent implements OnInit {
constructor(public translate: TranslateService, private toast: ToastService, private dialog: MatDialog) {}
- public ngOnInit(): void {
- this.getData();
- }
-
public removeMfa(type: MultiFactorType | SecondFactorType): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
@@ -73,14 +70,14 @@ export class FactorTableComponent implements OnInit {
req.setType(type as MultiFactorType);
(this.service as ManagementService).removeMultiFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
- this.refreshPageAfterTimout(2000);
+ this.listChanged.emit();
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new MgmtRemoveSecondFactorFromLoginPolicyRequest();
req.setType(type as SecondFactorType);
(this.service as ManagementService).removeSecondFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
- this.refreshPageAfterTimout(2000);
+ this.listChanged.emit();
});
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
@@ -89,14 +86,14 @@ export class FactorTableComponent implements OnInit {
req.setType(type as MultiFactorType);
(this.service as AdminService).removeMultiFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
- this.refreshPageAfterTimout(2000);
+ this.listChanged.emit();
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new AdminRemoveSecondFactorFromLoginPolicyRequest();
req.setType(type as SecondFactorType);
(this.service as AdminService).removeSecondFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
- this.refreshPageAfterTimout(2000);
+ this.listChanged.emit();
});
}
}
@@ -124,7 +121,8 @@ export class FactorTableComponent implements OnInit {
(this.service as ManagementService)
.addMultiFactorToLoginPolicy(req)
.then(() => {
- this.refreshPageAfterTimout(2000);
+ this.toast.showInfo('MFA.TOAST.ADDED', true);
+ this.listChanged.emit();
})
.catch((error) => {
this.toast.showError(error);
@@ -135,7 +133,8 @@ export class FactorTableComponent implements OnInit {
(this.service as ManagementService)
.addSecondFactorToLoginPolicy(req)
.then(() => {
- this.refreshPageAfterTimout(2000);
+ this.toast.showInfo('MFA.TOAST.ADDED', true);
+ this.listChanged.emit();
})
.catch((error) => {
this.toast.showError(error);
@@ -148,7 +147,8 @@ export class FactorTableComponent implements OnInit {
(this.service as AdminService)
.addMultiFactorToLoginPolicy(req)
.then(() => {
- this.refreshPageAfterTimout(2000);
+ this.toast.showInfo('MFA.TOAST.ADDED', true);
+ this.listChanged.emit();
})
.catch((error) => {
this.toast.showError(error);
@@ -159,7 +159,8 @@ export class FactorTableComponent implements OnInit {
(this.service as AdminService)
.addSecondFactorToLoginPolicy(req)
.then(() => {
- this.refreshPageAfterTimout(2000);
+ this.toast.showInfo('MFA.TOAST.ADDED', true);
+ this.listChanged.emit();
})
.catch((error) => {
this.toast.showError(error);
@@ -170,66 +171,6 @@ export class FactorTableComponent implements OnInit {
});
}
- private async getData(): Promise {
- this.loadingSubject.next(true);
-
- if (this.serviceType === PolicyComponentServiceType.MGMT) {
- if (this.componentType === LoginMethodComponentType.MultiFactor) {
- (this.service as ManagementService)
- .listLoginPolicyMultiFactors()
- .then((resp) => {
- this.mfas = resp.resultList;
- this.loadingSubject.next(false);
- })
- .catch((error) => {
- this.toast.showError(error);
- this.loadingSubject.next(false);
- });
- } else if (this.componentType === LoginMethodComponentType.SecondFactor) {
- (this.service as ManagementService)
- .listLoginPolicySecondFactors()
- .then((resp) => {
- this.mfas = resp.resultList;
- this.loadingSubject.next(false);
- })
- .catch((error) => {
- this.toast.showError(error);
- this.loadingSubject.next(false);
- });
- }
- } else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
- if (this.componentType === LoginMethodComponentType.MultiFactor) {
- (this.service as AdminService)
- .listLoginPolicyMultiFactors()
- .then((resp) => {
- this.mfas = resp.resultList;
- this.loadingSubject.next(false);
- })
- .catch((error) => {
- this.toast.showError(error);
- this.loadingSubject.next(false);
- });
- } else if (this.componentType === LoginMethodComponentType.SecondFactor) {
- (this.service as AdminService)
- .listLoginPolicySecondFactors()
- .then((resp) => {
- this.mfas = resp.resultList;
- this.loadingSubject.next(false);
- })
- .catch((error) => {
- this.toast.showError(error);
- this.loadingSubject.next(false);
- });
- }
- }
- }
-
- public refreshPageAfterTimout(to: number): void {
- setTimeout(() => {
- this.getData();
- }, to);
- }
-
public get availableSelection(): Array {
const allTypes: MultiFactorType[] | SecondFactorType[] =
this.componentType === LoginMethodComponentType.MultiFactor
@@ -238,7 +179,7 @@ export class FactorTableComponent implements OnInit {
? [SecondFactorType.SECOND_FACTOR_TYPE_U2F, SecondFactorType.SECOND_FACTOR_TYPE_OTP]
: [];
- const filtered = (allTypes as Array).filter((type) => !this.mfas.includes(type));
+ const filtered = (allTypes as Array).filter((type) => !this.list.includes(type));
return filtered;
}
diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.html b/console/src/app/modules/policies/login-policy/login-policy.component.html
index 6b76509464..946407da94 100644
--- a/console/src/app/modules/policies/login-policy/login-policy.component.html
+++ b/console/src/app/modules/policies/login-policy/login-policy.component.html
@@ -55,6 +55,8 @@
[service]="service"
[serviceType]="serviceType"
[componentType]="LoginMethodComponentType.MultiFactor"
+ [list]="loginData.multiFactorsList"
+ (listChanged)="fetchData()"
[disabled]="
loginData?.passwordlessType === PasswordlessType.PASSWORDLESS_TYPE_NOT_ALLOWED ||
([
@@ -103,6 +105,8 @@
[service]="service"
[serviceType]="serviceType"
[componentType]="LoginMethodComponentType.SecondFactor"
+ [list]="loginData.secondFactorsList"
+ (listChanged)="fetchData()"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.ts b/console/src/app/modules/policies/login-policy/login-policy.component.ts
index cb4c4d0d10..8f180b7d6f 100644
--- a/console/src/app/modules/policies/login-policy/login-policy.component.ts
+++ b/console/src/app/modules/policies/login-policy/login-policy.component.ts
@@ -57,7 +57,7 @@ export class LoginPolicyComponent implements OnInit {
});
}
- private fetchData(): void {
+ public fetchData(): void {
this.getData()
.then((resp) => {
if (resp.policy) {
@@ -147,6 +147,8 @@ export class LoginPolicyComponent implements OnInit {
mgmtreq.setForceMfa(this.loginData.forceMfa);
mgmtreq.setPasswordlessType(this.loginData.passwordlessType);
mgmtreq.setHidePasswordReset(this.loginData.hidePasswordReset);
+ mgmtreq.setMultiFactorsList(this.loginData.multiFactorsList);
+ mgmtreq.setSecondFactorsList(this.loginData.secondFactorsList);
const pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 240) * 60 * 60);
mgmtreq.setPasswordCheckLifetime(pcl);
diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md
index 9250bbfe5a..3498aa9aed 100644
--- a/docs/docs/apis/proto/management.md
+++ b/docs/docs/apis/proto/management.md
@@ -3046,6 +3046,21 @@ This is an empty request
| mfa_init_skip_lifetime | google.protobuf.Duration | - | |
| second_factor_check_lifetime | google.protobuf.Duration | - | |
| multi_factor_check_lifetime | google.protobuf.Duration | - | |
+| second_factors | repeated zitadel.policy.v1.SecondFactorType | - | |
+| multi_factors | repeated zitadel.policy.v1.MultiFactorType | - | |
+| idps | repeated AddCustomLoginPolicyRequest.IDP | - | |
+
+
+
+
+### AddCustomLoginPolicyRequest.IDP
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| idp_id | string | - | string.min_len: 1
string.max_len: 200
|
+| ownerType | zitadel.idp.v1.IDPOwnerType | - | enum.defined_only: true
enum.not_in: [0]
|
diff --git a/docs/docs/apis/proto/policy.md b/docs/docs/apis/proto/policy.md
index 2f519c8d3a..026836f72f 100644
--- a/docs/docs/apis/proto/policy.md
+++ b/docs/docs/apis/proto/policy.md
@@ -85,6 +85,9 @@ title: zitadel/policy.proto
| mfa_init_skip_lifetime | google.protobuf.Duration | - | |
| second_factor_check_lifetime | google.protobuf.Duration | - | |
| multi_factor_check_lifetime | google.protobuf.Duration | - | |
+| second_factors | repeated SecondFactorType | - | |
+| multi_factors | repeated MultiFactorType | - | |
+| idps | repeated zitadel.idp.v1.IDPLoginPolicyLink | - | |
diff --git a/internal/api/grpc/management/policy_login_converter.go b/internal/api/grpc/management/policy_login_converter.go
index f4f98d33f4..6b2c6cb88a 100644
--- a/internal/api/grpc/management/policy_login_converter.go
+++ b/internal/api/grpc/management/policy_login_converter.go
@@ -1,6 +1,7 @@
package management
import (
+ idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
"github.com/zitadel/zitadel/internal/domain"
@@ -23,8 +24,21 @@ func addLoginPolicyToDomain(p *mgmt_pb.AddCustomLoginPolicyRequest) *domain.Logi
MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(),
SecondFactorCheckLifetime: p.SecondFactorCheckLifetime.AsDuration(),
MultiFactorCheckLifetime: p.MultiFactorCheckLifetime.AsDuration(),
+ SecondFactors: policy_grpc.SecondFactorsTypesToDomain(p.SecondFactors),
+ MultiFactors: policy_grpc.MultiFactorsTypesToDomain(p.MultiFactors),
+ IDPProviders: addLoginPolicyIDPsToDomain(p.Idps),
}
}
+func addLoginPolicyIDPsToDomain(idps []*mgmt_pb.AddCustomLoginPolicyRequest_IDP) []*domain.IDPProvider {
+ providers := make([]*domain.IDPProvider, len(idps))
+ for i, idp := range idps {
+ providers[i] = &domain.IDPProvider{
+ Type: idp_grpc.IDPProviderTypeFromPb(idp.OwnerType),
+ IDPConfigID: idp.IdpId,
+ }
+ }
+ return providers
+}
func updateLoginPolicyToDomain(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *domain.LoginPolicy {
return &domain.LoginPolicy{
diff --git a/internal/api/grpc/policy/second_factor.go b/internal/api/grpc/policy/auth_factor.go
similarity index 65%
rename from internal/api/grpc/policy/second_factor.go
rename to internal/api/grpc/policy/auth_factor.go
index 84a61ea00a..56a155413c 100644
--- a/internal/api/grpc/policy/second_factor.go
+++ b/internal/api/grpc/policy/auth_factor.go
@@ -5,6 +5,14 @@ import (
policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy"
)
+func SecondFactorsTypesToDomain(secondFactorTypes []policy_pb.SecondFactorType) []domain.SecondFactorType {
+ types := make([]domain.SecondFactorType, len(secondFactorTypes))
+ for i, factorType := range secondFactorTypes {
+ types[i] = SecondFactorTypeToDomain(factorType)
+ }
+ return types
+}
+
func SecondFactorTypeToDomain(secondFactorType policy_pb.SecondFactorType) domain.SecondFactorType {
switch secondFactorType {
case policy_pb.SecondFactorType_SECOND_FACTOR_TYPE_OTP:
@@ -35,6 +43,23 @@ func ModelSecondFactorTypeToPb(secondFactorType domain.SecondFactorType) policy_
}
}
+func MultiFactorsTypesToDomain(multiFactorTypes []policy_pb.MultiFactorType) []domain.MultiFactorType {
+ types := make([]domain.MultiFactorType, len(multiFactorTypes))
+ for i, factorType := range multiFactorTypes {
+ types[i] = MultiFactorTypeToDomain(factorType)
+ }
+ return types
+}
+
+func MultiFactorTypeToDomain(multiFactorType policy_pb.MultiFactorType) domain.MultiFactorType {
+ switch multiFactorType {
+ case policy_pb.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION:
+ return domain.MultiFactorTypeU2FWithPIN
+ default:
+ return domain.MultiFactorTypeUnspecified
+ }
+}
+
func ModelMultiFactorTypesToPb(types []domain.MultiFactorType) []policy_pb.MultiFactorType {
t := make([]policy_pb.MultiFactorType, len(types))
for i, typ := range types {
diff --git a/internal/api/grpc/policy/login_policy.go b/internal/api/grpc/policy/login_policy.go
index 4b8d1c0376..b425bfc915 100644
--- a/internal/api/grpc/policy/login_policy.go
+++ b/internal/api/grpc/policy/login_policy.go
@@ -4,6 +4,7 @@ import (
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
+ idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/object"
@@ -26,6 +27,9 @@ func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy {
MfaInitSkipLifetime: durationpb.New(policy.MFAInitSkipLifetime),
SecondFactorCheckLifetime: durationpb.New(policy.SecondFactorCheckLifetime),
MultiFactorCheckLifetime: durationpb.New(policy.MultiFactorCheckLifetime),
+ SecondFactors: ModelSecondFactorTypesToPb(policy.SecondFactors),
+ MultiFactors: ModelMultiFactorTypesToPb(policy.MultiFactors),
+ Idps: idp_grpc.IDPLoginPolicyLinksToPb(policy.IDPLinks),
Details: &object.ObjectDetails{
Sequence: policy.Sequence,
CreationDate: timestamppb.New(policy.CreationDate),
diff --git a/internal/api/grpc/policy/multi_factor.go b/internal/api/grpc/policy/multi_factor.go
deleted file mode 100644
index 3064e5c586..0000000000
--- a/internal/api/grpc/policy/multi_factor.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package policy
-
-import (
- "github.com/zitadel/zitadel/internal/domain"
- policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy"
-)
-
-func MultiFactorTypeToDomain(multiFactorType policy_pb.MultiFactorType) domain.MultiFactorType {
- switch multiFactorType {
- case policy_pb.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION:
- return domain.MultiFactorTypeU2FWithPIN
- default:
- return domain.MultiFactorTypeUnspecified
- }
-}
diff --git a/internal/command/org_policy_login.go b/internal/command/org_policy_login.go
index ec8069608e..55dcfeb107 100644
--- a/internal/command/org_policy_login.go
+++ b/internal/command/org_policy_login.go
@@ -29,8 +29,7 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
}
orgAgg := OrgAggregateFromWriteModel(&addedPolicy.WriteModel)
- pushedEvents, err := c.eventstore.Push(
- ctx,
+ cmds := []eventstore.Command{
org.NewLoginPolicyAddedEvent(
ctx,
orgAgg,
@@ -46,7 +45,32 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
policy.ExternalLoginCheckLifetime,
policy.MFAInitSkipLifetime,
policy.SecondFactorCheckLifetime,
- policy.MultiFactorCheckLifetime))
+ policy.MultiFactorCheckLifetime),
+ }
+ for _, factor := range policy.SecondFactors {
+ if !factor.Valid() {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "Org-SFeea", "Errors.Org.LoginPolicy.MFA.Unspecified")
+ }
+ cmds = append(cmds, org.NewLoginPolicySecondFactorAddedEvent(ctx, orgAgg, factor))
+ }
+ for _, factor := range policy.MultiFactors {
+ if !factor.Valid() {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "Org-WSfrg", "Errors.Org.LoginPolicy.MFA.Unspecified")
+ }
+ cmds = append(cmds, org.NewLoginPolicyMultiFactorAddedEvent(ctx, orgAgg, factor))
+ }
+ for _, provider := range policy.IDPProviders {
+ if provider.Type == domain.IdentityProviderTypeOrg {
+ _, err = c.getOrgIDPConfigByID(ctx, provider.IDPConfigID, resourceOwner)
+ } else {
+ _, err = c.getInstanceIDPConfigByID(ctx, provider.IDPConfigID)
+ }
+ if err != nil {
+ return nil, caos_errs.ThrowPreconditionFailed(err, "Org-FEd32", "Errors.IDPConfig.NotExisting")
+ }
+ cmds = append(cmds, org.NewIdentityProviderAddedEvent(ctx, orgAgg, provider.IDPConfigID, provider.Type))
+ }
+ pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
diff --git a/internal/command/org_policy_login_test.go b/internal/command/org_policy_login_test.go
index 6d59061533..fe1fc7fc70 100644
--- a/internal/command/org_policy_login_test.go
+++ b/internal/command/org_policy_login_test.go
@@ -12,6 +12,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
+ "github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/policy"
"github.com/zitadel/zitadel/internal/repository/user"
@@ -183,6 +184,257 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
},
},
},
+ {
+ name: "add policy with invalid factors, invalid argument error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ policy: &domain.LoginPolicy{
+ AllowRegister: true,
+ AllowUsernamePassword: true,
+ AllowExternalIDP: true,
+ ForceMFA: true,
+ HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
+ PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
+ PasswordCheckLifetime: time.Hour * 1,
+ ExternalLoginCheckLifetime: time.Hour * 2,
+ MFAInitSkipLifetime: time.Hour * 3,
+ SecondFactorCheckLifetime: time.Hour * 4,
+ MultiFactorCheckLifetime: time.Hour * 5,
+ SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeUnspecified},
+ },
+ },
+ res: res{
+ err: caos_errs.IsErrorInvalidArgument,
+ },
+ },
+ {
+ name: "add policy factors,ok",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusher(
+ org.NewLoginPolicyAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ domain.PasswordlessTypeAllowed,
+ "https://example.com/redirect",
+ time.Hour*1,
+ time.Hour*2,
+ time.Hour*3,
+ time.Hour*4,
+ time.Hour*5,
+ ),
+ ),
+ eventFromEventPusher(
+ org.NewLoginPolicySecondFactorAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ domain.SecondFactorTypeOTP,
+ ),
+ ),
+ eventFromEventPusher(
+ org.NewLoginPolicyMultiFactorAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ domain.MultiFactorTypeU2FWithPIN,
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ policy: &domain.LoginPolicy{
+ AllowRegister: true,
+ AllowUsernamePassword: true,
+ AllowExternalIDP: true,
+ ForceMFA: true,
+ HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
+ PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
+ PasswordCheckLifetime: time.Hour * 1,
+ ExternalLoginCheckLifetime: time.Hour * 2,
+ MFAInitSkipLifetime: time.Hour * 3,
+ SecondFactorCheckLifetime: time.Hour * 4,
+ MultiFactorCheckLifetime: time.Hour * 5,
+ SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
+ MultiFactors: []domain.MultiFactorType{domain.MultiFactorTypeU2FWithPIN},
+ },
+ },
+ res: res{
+ want: &domain.LoginPolicy{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: "org1",
+ ResourceOwner: "org1",
+ },
+ AllowRegister: true,
+ AllowUsernamePassword: true,
+ AllowExternalIDP: true,
+ ForceMFA: true,
+ HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
+ PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
+ PasswordCheckLifetime: time.Hour * 1,
+ ExternalLoginCheckLifetime: time.Hour * 2,
+ MFAInitSkipLifetime: time.Hour * 3,
+ SecondFactorCheckLifetime: time.Hour * 4,
+ MultiFactorCheckLifetime: time.Hour * 5,
+ },
+ },
+ },
+ {
+ name: "add policy with unknown idp, invalid argument error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(),
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ policy: &domain.LoginPolicy{
+ AllowRegister: true,
+ AllowUsernamePassword: true,
+ AllowExternalIDP: true,
+ ForceMFA: true,
+ HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
+ PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
+ PasswordCheckLifetime: time.Hour * 1,
+ ExternalLoginCheckLifetime: time.Hour * 2,
+ MFAInitSkipLifetime: time.Hour * 3,
+ SecondFactorCheckLifetime: time.Hour * 4,
+ MultiFactorCheckLifetime: time.Hour * 5,
+ IDPProviders: []*domain.IDPProvider{
+ {
+ Type: domain.IdentityProviderTypeSystem,
+ IDPConfigID: "invalid",
+ },
+ },
+ },
+ },
+ res: res{
+ err: caos_errs.IsPreconditionFailed,
+ },
+ },
+ {
+ name: "add policy idp, ok",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(),
+ expectFilter(
+ eventFromEventPusher(
+ instance.NewIDPConfigAddedEvent(context.Background(),
+ &instance.NewAggregate("INSTANCE").Aggregate,
+ "config1",
+ "name1",
+ domain.IDPConfigTypeOIDC,
+ domain.IDPConfigStylingTypeGoogle,
+ true,
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusher(
+ org.NewLoginPolicyAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ domain.PasswordlessTypeAllowed,
+ "https://example.com/redirect",
+ time.Hour*1,
+ time.Hour*2,
+ time.Hour*3,
+ time.Hour*4,
+ time.Hour*5,
+ ),
+ ),
+ eventFromEventPusher(
+ org.NewIdentityProviderAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "config1",
+ domain.IdentityProviderTypeSystem,
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ policy: &domain.LoginPolicy{
+ AllowRegister: true,
+ AllowUsernamePassword: true,
+ AllowExternalIDP: true,
+ ForceMFA: true,
+ HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
+ PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
+ PasswordCheckLifetime: time.Hour * 1,
+ ExternalLoginCheckLifetime: time.Hour * 2,
+ MFAInitSkipLifetime: time.Hour * 3,
+ SecondFactorCheckLifetime: time.Hour * 4,
+ MultiFactorCheckLifetime: time.Hour * 5,
+ IDPProviders: []*domain.IDPProvider{
+ {
+ Type: domain.IdentityProviderTypeSystem,
+ IDPConfigID: "config1",
+ },
+ },
+ },
+ },
+ res: res{
+ want: &domain.LoginPolicy{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: "org1",
+ ResourceOwner: "org1",
+ },
+ AllowRegister: true,
+ AllowUsernamePassword: true,
+ AllowExternalIDP: true,
+ ForceMFA: true,
+ HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
+ PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
+ PasswordCheckLifetime: time.Hour * 1,
+ ExternalLoginCheckLifetime: time.Hour * 2,
+ MFAInitSkipLifetime: time.Hour * 3,
+ SecondFactorCheckLifetime: time.Hour * 4,
+ MultiFactorCheckLifetime: time.Hour * 5,
+ },
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/internal/query/login_policy.go b/internal/query/login_policy.go
index 69befcff73..86a8284b86 100644
--- a/internal/query/login_policy.go
+++ b/internal/query/login_policy.go
@@ -36,6 +36,7 @@ type LoginPolicy struct {
MFAInitSkipLifetime time.Duration
SecondFactorCheckLifetime time.Duration
MultiFactorCheckLifetime time.Duration
+ IDPLinks []*IDPLoginPolicyLink
}
type SecondFactors struct {
@@ -160,8 +161,11 @@ func (q *Queries) LoginPolicyByID(ctx context.Context, orgID string) (*LoginPoli
return nil, errors.ThrowInternal(err, "QUERY-scVHo", "Errors.Query.SQLStatement")
}
- row := q.client.QueryRowContext(ctx, stmt, args...)
- return scan(row)
+ rows, err := q.client.QueryContext(ctx, stmt, args...)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-SWgr3", "Errors.Internal")
+ }
+ return scan(rows)
}
func (q *Queries) DefaultLoginPolicy(ctx context.Context) (*LoginPolicy, error) {
@@ -174,8 +178,11 @@ func (q *Queries) DefaultLoginPolicy(ctx context.Context) (*LoginPolicy, error)
return nil, errors.ThrowInternal(err, "QUERY-t4TBK", "Errors.Query.SQLStatement")
}
- row := q.client.QueryRowContext(ctx, stmt, args...)
- return scan(row)
+ rows, err := q.client.QueryContext(ctx, stmt, args...)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-SArt2", "Errors.Internal")
+ }
+ return scan(rows)
}
func (q *Queries) SecondFactorsByOrg(ctx context.Context, orgID string) (*SecondFactors, error) {
@@ -278,7 +285,7 @@ func (q *Queries) DefaultMultiFactors(ctx context.Context) (*MultiFactors, error
return factors, err
}
-func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LoginPolicy, error)) {
+func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Rows) (*LoginPolicy, error)) {
return sq.Select(
LoginPolicyColumnOrgID.identifier(),
LoginPolicyColumnCreationDate.identifier(),
@@ -300,39 +307,69 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LoginPolicy,
LoginPolicyColumnMFAInitSkipLifetime.identifier(),
LoginPolicyColumnSecondFactorCheckLifetime.identifier(),
LoginPolicyColumnMultiFacotrCheckLifetime.identifier(),
- ).From(loginPolicyTable.identifier()).PlaceholderFormat(sq.Dollar),
- func(row *sql.Row) (*LoginPolicy, error) {
+ IDPLoginPolicyLinkIDPIDCol.identifier(),
+ IDPNameCol.identifier(),
+ IDPTypeCol.identifier(),
+ ).From(loginPolicyTable.identifier()).
+ LeftJoin(join(IDPLoginPolicyLinkIDPIDCol, LoginPolicyColumnOrgID)).
+ LeftJoin(join(IDPIDCol, IDPLoginPolicyLinkIDPIDCol)).
+ PlaceholderFormat(sq.Dollar),
+ func(rows *sql.Rows) (*LoginPolicy, error) {
p := new(LoginPolicy)
secondFactors := pq.Int32Array{}
multiFactors := pq.Int32Array{}
defaultRedirectURI := sql.NullString{}
- err := row.Scan(
- &p.OrgID,
- &p.CreationDate,
- &p.ChangeDate,
- &p.Sequence,
- &p.AllowRegister,
- &p.AllowUsernamePassword,
- &p.AllowExternalIDPs,
- &p.ForceMFA,
- &secondFactors,
- &multiFactors,
- &p.PasswordlessType,
- &p.IsDefault,
- &p.HidePasswordReset,
- &p.IgnoreUnknownUsernames,
- &defaultRedirectURI,
- &p.PasswordCheckLifetime,
- &p.ExternalLoginCheckLifetime,
- &p.MFAInitSkipLifetime,
- &p.SecondFactorCheckLifetime,
- &p.MultiFactorCheckLifetime,
- )
- if err != nil {
- if errs.Is(err, sql.ErrNoRows) {
- return nil, errors.ThrowNotFound(err, "QUERY-QsUBJ", "Errors.LoginPolicy.NotFound")
+ links := make([]*IDPLoginPolicyLink, 0)
+ for rows.Next() {
+ var (
+ idpID = sql.NullString{}
+ idpName = sql.NullString{}
+ idpType = sql.NullInt16{}
+ )
+ err := rows.Scan(
+ &p.OrgID,
+ &p.CreationDate,
+ &p.ChangeDate,
+ &p.Sequence,
+ &p.AllowRegister,
+ &p.AllowUsernamePassword,
+ &p.AllowExternalIDPs,
+ &p.ForceMFA,
+ &secondFactors,
+ &multiFactors,
+ &p.PasswordlessType,
+ &p.IsDefault,
+ &p.HidePasswordReset,
+ &p.IgnoreUnknownUsernames,
+ &defaultRedirectURI,
+ &p.PasswordCheckLifetime,
+ &p.ExternalLoginCheckLifetime,
+ &p.MFAInitSkipLifetime,
+ &p.SecondFactorCheckLifetime,
+ &p.MultiFactorCheckLifetime,
+ &idpID,
+ &idpName,
+ &idpType,
+ )
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-YcC53", "Errors.Internal")
}
- return nil, errors.ThrowInternal(err, "QUERY-YcC53", "Errors.Internal")
+ var link IDPLoginPolicyLink
+ if idpID.Valid {
+ link = IDPLoginPolicyLink{IDPID: idpID.String}
+
+ link.IDPName = idpName.String
+ //IDPType 0 is oidc so we have to set unspecified manually
+ if idpType.Valid {
+ link.IDPType = domain.IDPConfigType(idpType.Int16)
+ } else {
+ link.IDPType = domain.IDPConfigTypeUnspecified
+ }
+ links = append(links, &link)
+ }
+ }
+ if p.OrgID == "" {
+ return nil, errors.ThrowNotFound(nil, "QUERY-QsUBJ", "Errors.LoginPolicy.NotFound")
}
p.DefaultRedirectURI = defaultRedirectURI.String
p.MultiFactors = make([]domain.MultiFactorType, len(multiFactors))
@@ -343,6 +380,7 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LoginPolicy,
for i, mfa := range secondFactors {
p.SecondFactors[i] = domain.SecondFactorType(mfa)
}
+ p.IDPLinks = links
return p, nil
}
}
diff --git a/internal/query/login_policy_test.go b/internal/query/login_policy_test.go
index e586ecffb4..ecefa8fa95 100644
--- a/internal/query/login_policy_test.go
+++ b/internal/query/login_policy_test.go
@@ -50,8 +50,15 @@ func Test_LoginPolicyPrepares(t *testing.T) {
` projections.login_policies.external_login_check_lifetime,`+
` projections.login_policies.mfa_init_skip_lifetime,`+
` projections.login_policies.second_factor_check_lifetime,`+
- ` projections.login_policies.multi_factor_check_lifetime`+
- ` FROM projections.login_policies`),
+ ` projections.login_policies.multi_factor_check_lifetime,`+
+ ` projections.idp_login_policy_links.idp_id,`+
+ ` projections.idps.name,`+
+ ` projections.idps.type`+
+ ` FROM projections.login_policies`+
+ ` LEFT JOIN projections.idp_login_policy_links ON `+
+ ` projections.login_policies.aggregate_id = projections.idp_login_policy_links.idp_id`+
+ ` LEFT JOIN projections.idps ON`+
+ ` projections.idp_login_policy_links.idp_id = projections.idps.id`),
nil,
nil,
),
@@ -88,8 +95,15 @@ func Test_LoginPolicyPrepares(t *testing.T) {
` projections.login_policies.external_login_check_lifetime,`+
` projections.login_policies.mfa_init_skip_lifetime,`+
` projections.login_policies.second_factor_check_lifetime,`+
- ` projections.login_policies.multi_factor_check_lifetime`+
- ` FROM projections.login_policies`),
+ ` projections.login_policies.multi_factor_check_lifetime,`+
+ ` projections.idp_login_policy_links.idp_id,`+
+ ` projections.idps.name,`+
+ ` projections.idps.type`+
+ ` FROM projections.login_policies`+
+ ` LEFT JOIN projections.idp_login_policy_links ON `+
+ ` projections.login_policies.aggregate_id = projections.idp_login_policy_links.idp_id`+
+ ` LEFT JOIN projections.idps ON`+
+ ` projections.idp_login_policy_links.idp_id = projections.idps.id`),
[]string{
"aggregate_id",
"creation_date",
@@ -111,6 +125,9 @@ func Test_LoginPolicyPrepares(t *testing.T) {
"mfa_init_skip_lifetime",
"second_factor_check_lifetime",
"multi_factor_check_lifetime",
+ "idp_id",
+ "name",
+ "type",
},
[]driver.Value{
"ro",
@@ -133,6 +150,9 @@ func Test_LoginPolicyPrepares(t *testing.T) {
time.Hour * 2,
time.Hour * 2,
time.Hour * 2,
+ "config1",
+ "IDP",
+ domain.IDPConfigTypeJWT,
},
),
},
@@ -157,6 +177,13 @@ func Test_LoginPolicyPrepares(t *testing.T) {
MFAInitSkipLifetime: time.Hour * 2,
SecondFactorCheckLifetime: time.Hour * 2,
MultiFactorCheckLifetime: time.Hour * 2,
+ IDPLinks: []*IDPLoginPolicyLink{
+ {
+ IDPID: "config1",
+ IDPName: "IDP",
+ IDPType: domain.IDPConfigTypeJWT,
+ },
+ },
},
},
{
@@ -183,8 +210,15 @@ func Test_LoginPolicyPrepares(t *testing.T) {
` projections.login_policies.external_login_check_lifetime,`+
` projections.login_policies.mfa_init_skip_lifetime,`+
` projections.login_policies.second_factor_check_lifetime,`+
- ` projections.login_policies.multi_factor_check_lifetime`+
- ` FROM projections.login_policies`),
+ ` projections.login_policies.multi_factor_check_lifetime,`+
+ ` projections.idp_login_policy_links.idp_id,`+
+ ` projections.idps.name,`+
+ ` projections.idps.type`+
+ ` FROM projections.login_policies`+
+ ` LEFT JOIN projections.idp_login_policy_links ON `+
+ ` projections.login_policies.aggregate_id = projections.idp_login_policy_links.idp_id`+
+ ` LEFT JOIN projections.idps ON`+
+ ` projections.idp_login_policy_links.idp_id = projections.idps.id`),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
diff --git a/internal/query/projection/app.go b/internal/query/projection/app.go
index 60375af1ad..18c9ab168b 100644
--- a/internal/query/projection/app.go
+++ b/internal/query/projection/app.go
@@ -87,7 +87,7 @@ func NewAppProjection(ctx context.Context, config crdb.StatementHandlerConfig) *
crdb.NewColumn(AppAPIConfigColumnClientSecret, crdb.ColumnTypeJSONB, crdb.Nullable()),
crdb.NewColumn(AppAPIConfigColumnAuthMethod, crdb.ColumnTypeEnum),
},
- crdb.NewPrimaryKey(AppAPIConfigColumnAppID),
+ crdb.NewPrimaryKey(AppAPIConfigColumnAppID, AppAPIConfigColumnInstanceID),
appAPITableSuffix,
crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_api_ref_apps")),
crdb.WithIndex(crdb.NewIndex("client_id_idx", []string{AppAPIConfigColumnClientID})),
diff --git a/internal/query/projection/idp.go b/internal/query/projection/idp.go
index 27a64c6f67..5e05535c96 100644
--- a/internal/query/projection/idp.go
+++ b/internal/query/projection/idp.go
@@ -76,7 +76,7 @@ func NewIDPProjection(ctx context.Context, config crdb.StatementHandlerConfig) *
crdb.NewColumn(IDPStylingTypeCol, crdb.ColumnTypeEnum),
crdb.NewColumn(IDPOwnerTypeCol, crdb.ColumnTypeEnum),
crdb.NewColumn(IDPAutoRegisterCol, crdb.ColumnTypeBool, crdb.Default(false)),
- crdb.NewColumn(IDPTypeCol, crdb.ColumnTypeEnum),
+ crdb.NewColumn(IDPTypeCol, crdb.ColumnTypeEnum, crdb.Nullable()),
},
crdb.NewPrimaryKey(IDPIDCol, IDPInstanceIDCol),
crdb.WithIndex(crdb.NewIndex("ro_idx", []string{IDPResourceOwnerCol})),
@@ -92,9 +92,9 @@ func NewIDPProjection(ctx context.Context, config crdb.StatementHandlerConfig) *
crdb.NewColumn(OIDCConfigDisplayNameMappingCol, crdb.ColumnTypeEnum, crdb.Nullable()),
crdb.NewColumn(OIDCConfigUsernameMappingCol, crdb.ColumnTypeEnum, crdb.Nullable()),
crdb.NewColumn(OIDCConfigAuthorizationEndpointCol, crdb.ColumnTypeText, crdb.Nullable()),
- crdb.NewColumn(OIDCConfigTokenEndpointCol, crdb.ColumnTypeEnum, crdb.Nullable()),
+ crdb.NewColumn(OIDCConfigTokenEndpointCol, crdb.ColumnTypeText, crdb.Nullable()),
},
- crdb.NewPrimaryKey(OIDCConfigIDPIDCol),
+ crdb.NewPrimaryKey(OIDCConfigIDPIDCol, OIDCConfigInstanceIDCol),
IDPOIDCSuffix,
crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_oidc_ref_idp")),
),
@@ -106,7 +106,7 @@ func NewIDPProjection(ctx context.Context, config crdb.StatementHandlerConfig) *
crdb.NewColumn(JWTConfigHeaderNameCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(JWTConfigEndpointCol, crdb.ColumnTypeText, crdb.Nullable()),
},
- crdb.NewPrimaryKey(JWTConfigIDPIDCol),
+ crdb.NewPrimaryKey(JWTConfigIDPIDCol, JWTConfigInstanceIDCol),
IDPJWTSuffix,
crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_jwt_ref_idp")),
),
diff --git a/internal/query/projection/sms.go b/internal/query/projection/sms.go
index 7b6a78e0c7..f0c00da3fa 100644
--- a/internal/query/projection/sms.go
+++ b/internal/query/projection/sms.go
@@ -60,7 +60,7 @@ func NewSMSConfigProjection(ctx context.Context, config crdb.StatementHandlerCon
crdb.NewColumn(SMSTwilioConfigColumnSenderNumber, crdb.ColumnTypeText),
crdb.NewColumn(SMSTwilioConfigColumnToken, crdb.ColumnTypeJSONB),
},
- crdb.NewPrimaryKey(SMSTwilioConfigColumnSMSID),
+ crdb.NewPrimaryKey(SMSTwilioConfigColumnSMSID, SMSTwilioColumnInstanceID),
smsTwilioTableSuffix,
crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_twilio_ref_sms")),
),
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index b614fbc7ed..647bf1ade1 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -4337,6 +4337,11 @@ message GetDefaultLoginPolicyResponse {
}
message AddCustomLoginPolicyRequest {
+ message IDP {
+ string idp_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ zitadel.idp.v1.IDPOwnerType ownerType = 2 [(validate.rules).enum = {defined_only: true, not_in: [0]}];
+ }
+
bool allow_username_password = 1;
bool allow_register = 2;
bool allow_external_idp = 3;
@@ -4358,6 +4363,9 @@ message AddCustomLoginPolicyRequest {
google.protobuf.Duration mfa_init_skip_lifetime = 11;
google.protobuf.Duration second_factor_check_lifetime = 12;
google.protobuf.Duration multi_factor_check_lifetime = 13;
+ repeated zitadel.policy.v1.SecondFactorType second_factors = 14;
+ repeated zitadel.policy.v1.MultiFactorType multi_factors = 15;
+ repeated IDP idps = 16;
}
message AddCustomLoginPolicyResponse {
diff --git a/proto/zitadel/policy.proto b/proto/zitadel/policy.proto
index 905cd38f35..71b1ba0d7e 100644
--- a/proto/zitadel/policy.proto
+++ b/proto/zitadel/policy.proto
@@ -1,6 +1,7 @@
syntax = "proto3";
import "zitadel/object.proto";
+import "zitadel/idp.proto";
import "google/protobuf/duration.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
@@ -170,7 +171,9 @@ message LoginPolicy {
google.protobuf.Duration mfa_init_skip_lifetime = 13;
google.protobuf.Duration second_factor_check_lifetime = 14;
google.protobuf.Duration multi_factor_check_lifetime = 15;
-
+ repeated SecondFactorType second_factors = 16;
+ repeated MultiFactorType multi_factors = 17;
+ repeated zitadel.idp.v1.IDPLoginPolicyLink idps = 18;
}
enum SecondFactorType {