From 19621acfd3d8cb1700d33416fb39d70e794e3e3f Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 25 Jan 2023 09:49:41 +0100 Subject: [PATCH] feat: add notification policy and password change message (#5065) Implementation of new notification policy with functionality to send email when a password is changed --- cmd/defaults.yaml | 18 + .../message-texts/message-texts.component.ts | 85 +++- .../notification-policy.component.html | 49 +++ .../notification-policy.component.scss | 32 ++ .../notification-policy.component.spec.ts | 24 ++ .../notification-policy.component.ts | 177 ++++++++ .../notification-policy.module.ts | 43 ++ ...ssword-complexity-policy-routing.module.ts | 20 - .../password-complexity-policy.module.ts | 2 - .../settings-list.component.html | 5 + .../settings-list/settings-list.module.ts | 2 + .../src/app/modules/settings-list/settings.ts | 9 + .../org-settings/org-settings.component.ts | 2 + console/src/app/services/admin.service.ts | 45 ++ console/src/app/services/mgmt.service.ts | 58 +++ console/src/assets/i18n/de.json | 10 +- console/src/assets/i18n/en.json | 10 +- console/src/assets/i18n/fr.json | 10 +- console/src/assets/i18n/it.json | 10 +- console/src/assets/i18n/zh.json | 10 +- docs/docs/apis/proto/admin.md | 260 +++++++++++- docs/docs/apis/proto/management.md | 318 +++++++++++++- docs/docs/apis/proto/policy.md | 13 + .../manage/console/instance-settings.mdx | 28 +- .../img/guides/console/notification.png | Bin 0 -> 9619 bytes internal/api/grpc/admin/custom_text.go | 48 +++ .../api/grpc/admin/custom_text_converter.go | 15 + .../api/grpc/admin/notification_policy.go | 46 +++ internal/api/grpc/management/custom_text.go | 48 +++ .../grpc/management/custom_text_converter.go | 15 + .../grpc/management/policy_notification.go | 64 +++ .../api/grpc/policy/notification_policy.go | 20 + internal/api/ui/console/console.go | 4 + internal/command/instance.go | 4 + .../command/instance_policy_notification.go | 92 +++++ .../instance_policy_notification_model.go | 72 ++++ .../instance_policy_notification_test.go | 260 ++++++++++++ internal/command/org_converter.go | 1 + internal/command/org_policy_notification.go | 139 +++++++ .../command/org_policy_notification_model.go | 73 ++++ .../command/org_policy_notification_test.go | 391 ++++++++++++++++++ internal/command/policy_notification_model.go | 31 ++ internal/command/user_human_password.go | 17 + internal/domain/custom_message_text.go | 7 +- internal/notification/projection.go | 72 ++++ internal/notification/static/i18n/de.yaml | 7 + internal/notification/static/i18n/en.yaml | 7 + internal/notification/static/i18n/fr.yaml | 7 + internal/notification/static/i18n/it.yaml | 7 + internal/notification/static/i18n/zh.yaml | 7 + .../notification/types/password_change.go | 13 + internal/query/message_text.go | 3 + internal/query/notification_policy.go | 166 ++++++++ internal/query/notification_policy_test.go | 116 ++++++ internal/query/projection/message_texts.go | 3 +- .../query/projection/notification_policy.go | 187 +++++++++ .../projection/notification_policy_test.go | 258 ++++++++++++ internal/query/projection/projection.go | 3 + internal/repository/instance/eventstore.go | 4 +- .../instance/policy_notification.go | 73 ++++ internal/repository/org/eventstore.go | 5 +- .../repository/org/policy_notification.go | 102 +++++ .../repository/policy/policy_notification.go | 127 ++++++ internal/repository/user/eventstore.go | 1 + internal/repository/user/human_password.go | 29 ++ internal/static/i18n/de.yaml | 16 +- internal/static/i18n/en.yaml | 14 +- internal/static/i18n/fr.yaml | 12 + internal/static/i18n/it.yaml | 12 + internal/static/i18n/zh.yaml | 12 + proto/zitadel/admin.proto | 219 +++++++++- proto/zitadel/management.proto | 204 ++++++++- proto/zitadel/policy.proto | 6 + 73 files changed, 4196 insertions(+), 83 deletions(-) create mode 100644 console/src/app/modules/policies/notification-policy/notification-policy.component.html create mode 100644 console/src/app/modules/policies/notification-policy/notification-policy.component.scss create mode 100644 console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts create mode 100644 console/src/app/modules/policies/notification-policy/notification-policy.component.ts create mode 100644 console/src/app/modules/policies/notification-policy/notification-policy.module.ts delete mode 100644 console/src/app/modules/policies/password-complexity-policy/password-complexity-policy-routing.module.ts create mode 100644 docs/static/img/guides/console/notification.png create mode 100644 internal/api/grpc/admin/notification_policy.go create mode 100644 internal/api/grpc/management/policy_notification.go create mode 100644 internal/api/grpc/policy/notification_policy.go create mode 100644 internal/command/instance_policy_notification.go create mode 100644 internal/command/instance_policy_notification_model.go create mode 100644 internal/command/instance_policy_notification_test.go create mode 100644 internal/command/org_policy_notification.go create mode 100644 internal/command/org_policy_notification_model.go create mode 100644 internal/command/org_policy_notification_test.go create mode 100644 internal/command/policy_notification_model.go create mode 100644 internal/notification/types/password_change.go create mode 100644 internal/query/notification_policy.go create mode 100644 internal/query/notification_policy_test.go create mode 100644 internal/query/projection/notification_policy.go create mode 100644 internal/query/projection/notification_policy_test.go create mode 100644 internal/repository/instance/policy_notification.go create mode 100644 internal/repository/org/policy_notification.go create mode 100644 internal/repository/policy/policy_notification.go diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index bef7a07317..4b4c9ae3f8 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -436,6 +436,8 @@ DefaultInstance: TOSLink: https://zitadel.com/docs/legal/terms-of-service PrivacyLink: https://zitadel.com/docs/legal/privacy-policy HelpLink: "" + NotificationPolicy: + PasswordChange: true LabelPolicy: PrimaryColor: "#5469d4" BackgroundColor: "#fafafa" @@ -514,6 +516,14 @@ DefaultInstance: Greeting: Hallo {{.FirstName}} {{.LastName}}, Text: Die Domain {{.Domain}} wurde von einer Organisation beansprucht. Dein derzeitiger User {{.Username}} ist nicht Teil dieser Organisation. Daher musst du beim nächsten Login eine neue Email hinterlegen. Für diesen Login haben wir dir einen temporären Usernamen ({{.TempUsername}}) erstellt. ButtonText: Login + - MessageTextType: PasswordChange + Language: de + Title: ZITADEL - Passwort von Benutzer wurde geändert + PreHeader: Passwort Änderung + Subject: Passwort von Benutzer wurde geändert + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Das Password vom Benutzer wurde geändert. Wenn diese Änderung von jemand anderem gemacht wurde, empfehlen wir die sofortige Zurücksetzung ihres Passworts. + ButtonText: Login - MessageTextType: InitCode Language: en Title: Zitadel - Initialize User @@ -554,6 +564,14 @@ DefaultInstance: Greeting: Hello {{.FirstName}} {{.LastName}}, Text: The domain {{.Domain}} has been claimed by an organisation. Your current user {{.UserName}} is not part of this organisation. Therefore you'll have to change your email when you login. We have created a temporary username ({{.TempUsername}}) for this login. ButtonText: Login + - MessageTextType: PasswordChange + Language: en + Title: ZITADEL - Password of user has changed + PreHeader: Change password + Subject: Password of user has changed + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: The password of your user has changed. If this change was not done by you, please be advised to immediately reset your password. + ButtonText: Login InternalAuthZ: RolePermissionMappings: diff --git a/console/src/app/modules/policies/message-texts/message-texts.component.ts b/console/src/app/modules/policies/message-texts/message-texts.component.ts index b1d73c0502..cc8dc84440 100644 --- a/console/src/app/modules/policies/message-texts/message-texts.component.ts +++ b/console/src/app/modules/policies/message-texts/message-texts.component.ts @@ -3,31 +3,39 @@ import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { MatLegacySelectChange as MatSelectChange } from '@angular/material/legacy-select'; import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs'; import { - GetCustomPasswordResetMessageTextRequest as AdminGetCustomPasswordResetMessageTextRequest, - GetDefaultInitMessageTextRequest as AdminGetDefaultInitMessageTextRequest, - GetDefaultVerifyEmailMessageTextRequest as AdminGetDefaultVerifyEmailMessageTextRequest, - GetDefaultVerifyPhoneMessageTextRequest as AdminGetDefaultVerifyPhoneMessageTextRequest, SetDefaultDomainClaimedMessageTextRequest, SetDefaultInitMessageTextRequest, + SetDefaultPasswordChangeMessageTextRequest, SetDefaultPasswordlessRegistrationMessageTextRequest, SetDefaultPasswordResetMessageTextRequest, SetDefaultVerifyEmailMessageTextRequest, SetDefaultVerifyPhoneMessageTextRequest, + GetDefaultPasswordChangeMessageTextRequest as AdminGetDefaultPasswordChangeMessageTextRequest, + GetDefaultInitMessageTextRequest as AdminGetDefaultInitMessageTextRequest, + GetDefaultVerifyEmailMessageTextRequest as AdminGetDefaultVerifyEmailMessageTextRequest, + GetDefaultVerifyPhoneMessageTextRequest as AdminGetDefaultVerifyPhoneMessageTextRequest, + GetDefaultPasswordResetMessageTextRequest as AdminGetDefaultPasswordResetMessageTextRequest, + GetDefaultDomainClaimedMessageTextRequest as AdminGetDefaultDomainClaimedMessageTextRequest, + GetDefaultPasswordlessRegistrationMessageTextRequest as AdminGetDefaultPasswordlessRegistrationMessageTextRequest, } from 'src/app/proto/generated/zitadel/admin_pb'; import { GetCustomDomainClaimedMessageTextRequest, + GetCustomInitMessageTextRequest, + GetCustomPasswordChangeMessageTextRequest, GetCustomPasswordlessRegistrationMessageTextRequest, GetCustomPasswordResetMessageTextRequest, GetCustomVerifyEmailMessageTextRequest, GetCustomVerifyPhoneMessageTextRequest, GetDefaultDomainClaimedMessageTextRequest, GetDefaultInitMessageTextRequest, + GetDefaultPasswordChangeMessageTextRequest, GetDefaultPasswordlessRegistrationMessageTextRequest, GetDefaultPasswordResetMessageTextRequest, GetDefaultVerifyEmailMessageTextRequest, GetDefaultVerifyPhoneMessageTextRequest, SetCustomDomainClaimedMessageTextRequest, SetCustomInitMessageTextRequest, + SetCustomPasswordChangeMessageTextRequest, SetCustomPasswordlessRegistrationMessageTextRequest, SetCustomPasswordResetMessageTextRequest, SetCustomVerifyEmailMessageTextRequest, @@ -50,12 +58,30 @@ enum MESSAGETYPES { PASSWORDRESET = 'PR', DOMAINCLAIMED = 'DC', PASSWORDLESS = 'PL', + PASSWORDCHANGE = 'PC', } const REQUESTMAP = { [PolicyComponentServiceType.MGMT]: { + [MESSAGETYPES.PASSWORDCHANGE]: { + get: new GetCustomPasswordChangeMessageTextRequest(), + set: new SetCustomPasswordChangeMessageTextRequest(), + getDefault: new GetDefaultPasswordChangeMessageTextRequest(), + setFcn: (map: Partial): SetCustomPasswordChangeMessageTextRequest => { + const req = new SetCustomPasswordChangeMessageTextRequest(); + req.setButtonText(map.buttonText ?? ''); + req.setFooterText(map.footerText ?? ''); + req.setGreeting(map.greeting ?? ''); + req.setPreHeader(map.preHeader ?? ''); + req.setSubject(map.subject ?? ''); + req.setText(map.text ?? ''); + req.setTitle(map.title ?? ''); + + return req; + }, + }, [MESSAGETYPES.INIT]: { - get: new GetDefaultInitMessageTextRequest(), + get: new GetCustomInitMessageTextRequest(), set: new SetCustomInitMessageTextRequest(), getDefault: new GetDefaultInitMessageTextRequest(), setFcn: (map: Partial): SetCustomInitMessageTextRequest => { @@ -164,6 +190,22 @@ const REQUESTMAP = { }, }, [PolicyComponentServiceType.ADMIN]: { + [MESSAGETYPES.PASSWORDCHANGE]: { + get: new AdminGetDefaultPasswordChangeMessageTextRequest(), + set: new SetDefaultPasswordChangeMessageTextRequest(), + setFcn: (map: Partial): SetDefaultPasswordChangeMessageTextRequest => { + const req = new SetDefaultPasswordChangeMessageTextRequest(); + req.setButtonText(map.buttonText ?? ''); + req.setFooterText(map.footerText ?? ''); + req.setGreeting(map.greeting ?? ''); + req.setPreHeader(map.preHeader ?? ''); + req.setSubject(map.subject ?? ''); + req.setText(map.text ?? ''); + req.setTitle(map.title ?? ''); + + return req; + }, + }, [MESSAGETYPES.INIT]: { get: new AdminGetDefaultInitMessageTextRequest(), set: new SetDefaultInitMessageTextRequest(), @@ -213,7 +255,7 @@ const REQUESTMAP = { }, }, [MESSAGETYPES.PASSWORDRESET]: { - get: new AdminGetCustomPasswordResetMessageTextRequest(), + get: new AdminGetDefaultPasswordResetMessageTextRequest(), set: new SetDefaultPasswordResetMessageTextRequest(), setFcn: ( map: Partial, @@ -231,7 +273,7 @@ const REQUESTMAP = { }, }, [MESSAGETYPES.DOMAINCLAIMED]: { - get: new GetDefaultDomainClaimedMessageTextRequest(), + get: new AdminGetDefaultDomainClaimedMessageTextRequest(), set: new SetDefaultDomainClaimedMessageTextRequest(), setFcn: ( map: Partial, @@ -249,7 +291,7 @@ const REQUESTMAP = { }, }, [MESSAGETYPES.PASSWORDLESS]: { - get: new GetDefaultPasswordlessRegistrationMessageTextRequest(), + get: new AdminGetDefaultPasswordlessRegistrationMessageTextRequest(), set: new SetDefaultPasswordlessRegistrationMessageTextRequest(), setFcn: ( map: Partial, @@ -382,6 +424,20 @@ export class MessageTextsComponent implements OnInit, OnDestroy { { key: 'POLICY.MESSAGE_TEXTS.CHIPS.loginnames', value: '{{.LoginNames}}' }, { key: 'POLICY.MESSAGE_TEXTS.CHIPS.changedate', value: '{{.ChangeDate}}' }, ], + [MESSAGETYPES.PASSWORDCHANGE]: [ + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.preferredLoginName', value: '{{.PreferredLoginName}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.Lastname}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.verifiedEmail', value: '{{.VerifiedEmail}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastPhone', value: '{{.LastPhone}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.verifiedPhone', value: '{{.VerifiedPhone}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.loginnames', value: '{{.LoginNames}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.changedate', value: '{{.ChangeDate}}' }, + ], }; public locale: string = 'en'; @@ -435,6 +491,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy { return this.stripDetails(this.service.getDefaultDomainClaimedMessageText(req)); case MESSAGETYPES.PASSWORDLESS: return this.stripDetails(this.service.getDefaultPasswordlessRegistrationMessageText(req)); + case MESSAGETYPES.PASSWORDCHANGE: + return this.stripDetails(this.service.getDefaultPasswordChangeMessageText(req)); } } @@ -453,6 +511,9 @@ export class MessageTextsComponent implements OnInit, OnDestroy { return this.stripDetails((this.service as ManagementService).getCustomDomainClaimedMessageText(req)); case MESSAGETYPES.PASSWORDLESS: return this.stripDetails((this.service as ManagementService).getCustomPasswordlessRegistrationMessageText(req)); + case MESSAGETYPES.PASSWORDCHANGE: + return this.stripDetails((this.service as ManagementService).getCustomPasswordChangeMessageText(req)); + default: return undefined; } @@ -470,6 +531,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy { return this.stripDetails((this.service as AdminService).getCustomDomainClaimedMessageText(req)); case MESSAGETYPES.PASSWORDLESS: return this.stripDetails((this.service as AdminService).getCustomPasswordlessRegistrationMessageText(req)); + case MESSAGETYPES.PASSWORDCHANGE: + return this.stripDetails((this.service as AdminService).getCustomPasswordChangeMessageText(req)); default: return undefined; } @@ -535,6 +598,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy { return handler( (this.service as ManagementService).getCustomPasswordlessRegistrationMessageText(this.updateRequest), ); + case MESSAGETYPES.PASSWORDCHANGE: + return handler((this.service as ManagementService).getCustomPasswordChangeMessageText(this.updateRequest)); } } else if (this.serviceType === PolicyComponentServiceType.ADMIN) { switch (this.currentType) { @@ -550,6 +615,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy { return handler((this.service as AdminService).setDefaultDomainClaimedMessageText(this.updateRequest)); case MESSAGETYPES.PASSWORDLESS: return handler((this.service as AdminService).setDefaultPasswordlessRegistrationMessageText(this.updateRequest)); + case MESSAGETYPES.PASSWORDCHANGE: + return handler((this.service as AdminService).setDefaultPasswordChangeMessageText(this.updateRequest)); } } } @@ -595,6 +662,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy { return handler( (this.service as ManagementService).resetCustomPasswordlessRegistrationMessageTextToDefault(this.locale), ); + case MESSAGETYPES.PASSWORDCHANGE: + return handler((this.service as ManagementService).resetCustomPasswordChangeMessageTextToDefault(this.locale)); default: return Promise.reject(); } diff --git a/console/src/app/modules/policies/notification-policy/notification-policy.component.html b/console/src/app/modules/policies/notification-policy/notification-policy.component.html new file mode 100644 index 0000000000..786fe4ec14 --- /dev/null +++ b/console/src/app/modules/policies/notification-policy/notification-policy.component.html @@ -0,0 +1,49 @@ +

{{ 'POLICY.NOTIFICATION.TITLE' | translate }}

+

{{ 'POLICY.NOTIFICATION.DESCRIPTION' | translate }}

+ +
+ +
+ + + + + +
+ +
+
+ + {{ 'POLICY.NOTIFICATION.PASSWORDCHANGE' | translate }} + +
+
+
+
+ +
+ +
diff --git a/console/src/app/modules/policies/notification-policy/notification-policy.component.scss b/console/src/app/modules/policies/notification-policy/notification-policy.component.scss new file mode 100644 index 0000000000..ba37cdc7cb --- /dev/null +++ b/console/src/app/modules/policies/notification-policy/notification-policy.component.scss @@ -0,0 +1,32 @@ +.spinner-wr { + margin: 0.5rem 0; +} + +.policy-applied-to { + margin: -1rem 0 0 0; + font-size: 14px; +} + +.notification-policy-card { + max-width: 400px; + + .notification-policy-content { + display: flex; + flex-direction: column; + width: 100%; + + .row { + display: flex; + align-items: center; + } + } +} + +.btn-container { + display: flex; + justify-content: flex-start; + + button { + display: block; + } +} diff --git a/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts b/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts new file mode 100644 index 0000000000..c323d884f1 --- /dev/null +++ b/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component'; + +describe('PasswordComplexityPolicyComponent', () => { + let component: PasswordComplexityPolicyComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [PasswordComplexityPolicyComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordComplexityPolicyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/console/src/app/modules/policies/notification-policy/notification-policy.component.ts b/console/src/app/modules/policies/notification-policy/notification-policy.component.ts new file mode 100644 index 0000000000..3787d3aa47 --- /dev/null +++ b/console/src/app/modules/policies/notification-policy/notification-policy.component.ts @@ -0,0 +1,177 @@ +import { Component, Injector, Input, OnInit, Type } from '@angular/core'; +import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { + AddNotificationPolicyRequest, + GetNotificationPolicyResponse as AdminGetNotificationPolicyResponse, + UpdateNotificationPolicyRequest, +} from 'src/app/proto/generated/zitadel/admin_pb'; +import { + AddCustomNotificationPolicyRequest, + GetNotificationPolicyResponse as MgmtGetNotificationPolicyResponse, +} from 'src/app/proto/generated/zitadel/management_pb'; +import { NotificationPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; +import { AdminService } from 'src/app/services/admin.service'; +import { ManagementService } from 'src/app/services/mgmt.service'; +import { ToastService } from 'src/app/services/toast.service'; + +import { InfoSectionType } from '../../info-section/info-section.component'; +import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component'; +import { PolicyComponentServiceType } from '../policy-component-types.enum'; + +@Component({ + selector: 'cnsl-notification-policy', + templateUrl: './notification-policy.component.html', + styleUrls: ['./notification-policy.component.scss'], +}) +export class NotificationPolicyComponent implements OnInit { + @Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; + public service!: ManagementService | AdminService; + + public notificationData?: NotificationPolicy.AsObject = { isDefault: false, passwordChange: false }; + + public PolicyComponentServiceType: any = PolicyComponentServiceType; + + public loading: boolean = false; + public InfoSectionType: any = InfoSectionType; + + public isDefault: boolean = false; + private hasNotificationPolicy: boolean = false; + constructor(private toast: ToastService, private injector: Injector, private dialog: MatDialog) {} + + public ngOnInit(): void { + switch (this.serviceType) { + case PolicyComponentServiceType.MGMT: + this.service = this.injector.get(ManagementService as Type); + break; + case PolicyComponentServiceType.ADMIN: + this.service = this.injector.get(AdminService as Type); + break; + } + this.fetchData(); + } + + public fetchData(): void { + this.loading = true; + + this.getData() + .then((data) => { + if (data.policy) { + this.hasNotificationPolicy = true; + this.notificationData = data.policy; + this.isDefault = data.policy.isDefault; + this.loading = false; + } + }) + .catch((error) => { + this.loading = false; + if (error && error.code === 5) { + console.log(error); + this.hasNotificationPolicy = false; + } + }); + } + + private async getData(): Promise< + MgmtGetNotificationPolicyResponse.AsObject | AdminGetNotificationPolicyResponse.AsObject + > { + switch (this.serviceType) { + case PolicyComponentServiceType.MGMT: + return (this.service as ManagementService).getNotificationPolicy(); + case PolicyComponentServiceType.ADMIN: + return (this.service as AdminService).getNotificationPolicy(); + } + } + + public removePolicy(): void { + if (this.service instanceof ManagementService) { + const dialogRef = this.dialog.open(WarnDialogComponent, { + data: { + confirmKey: 'ACTIONS.RESET', + cancelKey: 'ACTIONS.CANCEL', + titleKey: 'SETTING.DIALOG.RESET.DEFAULTTITLE', + descriptionKey: 'SETTING.DIALOG.RESET.DEFAULTDESCRIPTION', + }, + width: '400px', + }); + + dialogRef.afterClosed().subscribe((resp) => { + if (resp) { + (this.service as ManagementService) + .resetNotificationPolicyToDefault() + .then(() => { + this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true); + this.isDefault = true; + setTimeout(() => { + this.fetchData(); + }, 1000); + }) + .catch((error) => { + this.toast.showError(error); + }); + } + }); + } + } + + public savePolicy(): void { + if (this.notificationData) { + switch (this.serviceType) { + case PolicyComponentServiceType.MGMT: + if ((this.notificationData as NotificationPolicy.AsObject).isDefault) { + const req = new AddCustomNotificationPolicyRequest(); + req.setPasswordChange(this.notificationData.passwordChange); + (this.service as ManagementService) + .addCustomNotificationPolicy(req) + .then(() => { + this.isDefault = false; + this.toast.showInfo('POLICY.TOAST.SET', true); + }) + .catch((error) => { + this.toast.showError(error); + }); + } else { + const req = new UpdateNotificationPolicyRequest(); + req.setPasswordChange(this.notificationData.passwordChange); + (this.service as ManagementService) + .updateCustomNotificationPolicy(req) + .then(() => { + this.isDefault = false; + this.toast.showInfo('POLICY.TOAST.SET', true); + }) + .catch((error) => { + this.toast.showError(error); + }); + } + break; + case PolicyComponentServiceType.ADMIN: + if (this.hasNotificationPolicy) { + const req = new UpdateNotificationPolicyRequest(); + req.setPasswordChange(this.notificationData.passwordChange); + (this.service as AdminService) + .updateNotificationPolicy(req) + .then(() => { + this.isDefault = false; + this.toast.showInfo('POLICY.TOAST.SET', true); + }) + .catch((error) => { + this.toast.showError(error); + }); + } else { + const req = new AddNotificationPolicyRequest(); + req.setPasswordChange(this.notificationData.passwordChange); + (this.service as AdminService) + .addNotificationPolicy(req) + .then(() => { + this.isDefault = false; + this.hasNotificationPolicy = true; + this.toast.showInfo('POLICY.TOAST.SET', true); + }) + .catch((error) => { + this.toast.showError(error); + }); + } + break; + } + } + } +} diff --git a/console/src/app/modules/policies/notification-policy/notification-policy.module.ts b/console/src/app/modules/policies/notification-policy/notification-policy.module.ts new file mode 100644 index 0000000000..9915e44fc3 --- /dev/null +++ b/console/src/app/modules/policies/notification-policy/notification-policy.module.ts @@ -0,0 +1,43 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; +import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; +import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; +import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { TranslateModule } from '@ngx-translate/core'; +import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; +import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module'; +import { InputModule } from 'src/app/modules/input/input.module'; +import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; + +import { CardModule } from '../../card/card.module'; +import { InfoSectionModule } from '../../info-section/info-section.module'; +import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module'; +import { NotificationPolicyComponent } from './notification-policy.component'; + +@NgModule({ + declarations: [NotificationPolicyComponent], + imports: [ + CommonModule, + FormsModule, + InputModule, + MatButtonModule, + MatIconModule, + HasRoleModule, + MatDialogModule, + MatTooltipModule, + MatCheckboxModule, + HasRolePipeModule, + TranslateModule, + WarnDialogModule, + DetailLayoutModule, + CardModule, + MatProgressSpinnerModule, + InfoSectionModule, + ], + exports: [NotificationPolicyComponent], +}) +export class NotificationPolicyModule {} diff --git a/console/src/app/modules/policies/password-complexity-policy/password-complexity-policy-routing.module.ts b/console/src/app/modules/policies/password-complexity-policy/password-complexity-policy-routing.module.ts deleted file mode 100644 index c3db22004f..0000000000 --- a/console/src/app/modules/policies/password-complexity-policy/password-complexity-policy-routing.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; - -import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component'; - -const routes: Routes = [ - { - path: '', - component: PasswordComplexityPolicyComponent, - data: { - animation: 'DetailPage', - }, - }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class PasswordComplexityPolicyRoutingModule {} diff --git a/console/src/app/modules/policies/password-complexity-policy/password-complexity-policy.module.ts b/console/src/app/modules/policies/password-complexity-policy/password-complexity-policy.module.ts index 2f83f8331a..2ddcf066c7 100644 --- a/console/src/app/modules/policies/password-complexity-policy/password-complexity-policy.module.ts +++ b/console/src/app/modules/policies/password-complexity-policy/password-complexity-policy.module.ts @@ -16,13 +16,11 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod import { CardModule } from '../../card/card.module'; import { InfoSectionModule } from '../../info-section/info-section.module'; import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module'; -import { PasswordComplexityPolicyRoutingModule } from './password-complexity-policy-routing.module'; import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component'; @NgModule({ declarations: [PasswordComplexityPolicyComponent], imports: [ - PasswordComplexityPolicyRoutingModule, CommonModule, FormsModule, InputModule, diff --git a/console/src/app/modules/settings-list/settings-list.component.html b/console/src/app/modules/settings-list/settings-list.component.html index 15097f4aa8..74b535a463 100644 --- a/console/src/app/modules/settings-list/settings-list.component.html +++ b/console/src/app/modules/settings-list/settings-list.component.html @@ -25,9 +25,14 @@ + + + + diff --git a/console/src/app/modules/settings-list/settings-list.module.ts b/console/src/app/modules/settings-list/settings-list.module.ts index 02ff6e5b33..a9103f733a 100644 --- a/console/src/app/modules/settings-list/settings-list.module.ts +++ b/console/src/app/modules/settings-list/settings-list.module.ts @@ -11,6 +11,7 @@ import { IdpSettingsModule } from '../policies/idp-settings/idp-settings.module' import { LoginPolicyModule } from '../policies/login-policy/login-policy.module'; import { LoginTextsPolicyModule } from '../policies/login-texts/login-texts.module'; import { MessageTextsPolicyModule } from '../policies/message-texts/message-texts.module'; +import { NotificationPolicyModule } from '../policies/notification-policy/notification-policy.module'; import { NotificationSettingsModule } from '../policies/notification-settings/notification-settings.module'; import { OIDCConfigurationModule } from '../policies/oidc-configuration/oidc-configuration.module'; import { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module'; @@ -34,6 +35,7 @@ import { SettingsListComponent } from './settings-list.component'; PasswordLockoutPolicyModule, PrivateLabelingPolicyModule, GeneralSettingsModule, + NotificationPolicyModule, IdpSettingsModule, PrivacyPolicyModule, MessageTextsPolicyModule, diff --git a/console/src/app/modules/settings-list/settings.ts b/console/src/app/modules/settings-list/settings.ts index d978ce2396..806ce3de2d 100644 --- a/console/src/app/modules/settings-list/settings.ts +++ b/console/src/app/modules/settings-list/settings.ts @@ -92,6 +92,15 @@ export const NOTIFICATIONS: SidenavSetting = { }, }; +export const NOTIFICATION_POLICY: SidenavSetting = { + id: 'notifications', + i18nKey: 'SETTINGS.LIST.NOTIFICATIONS', + groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS', + requiredRoles: { + [PolicyComponentServiceType.MGMT]: ['policy.read'], + }, +}; + export const MESSAGETEXTS: SidenavSetting = { id: 'messagetexts', i18nKey: 'SETTINGS.LIST.MESSAGETEXTS', diff --git a/console/src/app/pages/org-settings/org-settings.component.ts b/console/src/app/pages/org-settings/org-settings.component.ts index 0caa17611c..aeaac06b44 100644 --- a/console/src/app/pages/org-settings/org-settings.component.ts +++ b/console/src/app/pages/org-settings/org-settings.component.ts @@ -11,6 +11,7 @@ import { DOMAIN, IDP, LOCKOUT, + NOTIFICATION_POLICY, LOGIN, LOGINTEXTS, MESSAGETEXTS, @@ -30,6 +31,7 @@ export class OrgSettingsComponent { IDP, COMPLEXITY, LOCKOUT, + NOTIFICATION_POLICY, DOMAIN, BRANDING, MESSAGETEXTS, diff --git a/console/src/app/services/admin.service.ts b/console/src/app/services/admin.service.ts index 35c521ff7c..93f42177ef 100644 --- a/console/src/app/services/admin.service.ts +++ b/console/src/app/services/admin.service.ts @@ -204,6 +204,18 @@ import { GetSecurityPolicyResponse, SetSecurityPolicyRequest, SetSecurityPolicyResponse, + GetNotificationPolicyRequest, + GetNotificationPolicyResponse, + UpdateNotificationPolicyRequest, + UpdateNotificationPolicyResponse, + GetDefaultPasswordChangeMessageTextResponse, + GetDefaultPasswordChangeMessageTextRequest, + GetCustomPasswordChangeMessageTextResponse, + SetDefaultPasswordChangeMessageTextRequest, + SetDefaultPasswordChangeMessageTextResponse, + GetCustomPasswordChangeMessageTextRequest, + AddNotificationPolicyRequest, + AddNotificationPolicyResponse, } from '../proto/generated/zitadel/admin_pb'; import { SearchQuery } from '../proto/generated/zitadel/member_pb'; import { ListQuery } from '../proto/generated/zitadel/object_pb'; @@ -346,6 +358,24 @@ export class AdminService { return this.grpcService.admin.setDefaultPasswordlessRegistrationMessageText(req, null).then((resp) => resp.toObject()); } + public getDefaultPasswordChangeMessageText( + req: GetDefaultPasswordChangeMessageTextRequest, + ): Promise { + return this.grpcService.admin.getDefaultPasswordChangeMessageText(req, null).then((resp) => resp.toObject()); + } + + public getCustomPasswordChangeMessageText( + req: GetCustomPasswordChangeMessageTextRequest, + ): Promise { + return this.grpcService.admin.getCustomPasswordChangeMessageText(req, null).then((resp) => resp.toObject()); + } + + public setDefaultPasswordChangeMessageText( + req: SetDefaultPasswordChangeMessageTextRequest, + ): Promise { + return this.grpcService.admin.setDefaultPasswordChangeMessageText(req, null).then((resp) => resp.toObject()); + } + public SetUpOrg(org: SetUpOrgRequest.Org, human: SetUpOrgRequest.Human): Promise { const req = new SetUpOrgRequest(); @@ -484,6 +514,21 @@ export class AdminService { return this.grpcService.admin.setDefaultLanguage(req, null).then((resp) => resp.toObject()); } + /* notification policy */ + + public getNotificationPolicy(): Promise { + const req = new GetNotificationPolicyRequest(); + return this.grpcService.admin.getNotificationPolicy(req, null).then((resp) => resp.toObject()); + } + + public updateNotificationPolicy(req: UpdateNotificationPolicyRequest): Promise { + return this.grpcService.admin.updateNotificationPolicy(req, null).then((resp) => resp.toObject()); + } + + public addNotificationPolicy(req: AddNotificationPolicyRequest): Promise { + return this.grpcService.admin.addNotificationPolicy(req, null).then((resp) => resp.toObject()); + } + /* security policy */ public getSecurityPolicy(): Promise { diff --git a/console/src/app/services/mgmt.service.ts b/console/src/app/services/mgmt.service.ts index 706e9a02ea..ee3840caca 100644 --- a/console/src/app/services/mgmt.service.ts +++ b/console/src/app/services/mgmt.service.ts @@ -24,6 +24,8 @@ import { AddCustomLockoutPolicyResponse, AddCustomLoginPolicyRequest, AddCustomLoginPolicyResponse, + AddCustomNotificationPolicyRequest, + AddCustomNotificationPolicyResponse, AddCustomPasswordAgePolicyRequest, AddCustomPasswordAgePolicyResponse, AddCustomPasswordComplexityPolicyRequest, @@ -108,6 +110,8 @@ import { GetCustomInitMessageTextResponse, GetCustomLoginTextsRequest, GetCustomLoginTextsResponse, + GetCustomPasswordChangeMessageTextRequest, + GetCustomPasswordChangeMessageTextResponse, GetCustomPasswordlessRegistrationMessageTextRequest, GetCustomPasswordlessRegistrationMessageTextResponse, GetCustomPasswordResetMessageTextRequest, @@ -124,6 +128,8 @@ import { GetDefaultLabelPolicyResponse, GetDefaultLoginTextsRequest, GetDefaultLoginTextsResponse, + GetDefaultPasswordChangeMessageTextRequest, + GetDefaultPasswordChangeMessageTextResponse, GetDefaultPasswordComplexityPolicyRequest, GetDefaultPasswordComplexityPolicyResponse, GetDefaultPasswordlessRegistrationMessageTextRequest, @@ -156,6 +162,8 @@ import { GetLoginPolicyResponse, GetMyOrgRequest, GetMyOrgResponse, + GetNotificationPolicyRequest, + GetNotificationPolicyResponse, GetOIDCInformationRequest, GetOIDCInformationResponse, GetOrgByDomainGlobalRequest, @@ -343,6 +351,8 @@ import { ResetCustomInitMessageTextToDefaultResponse, ResetCustomLoginTextsToDefaultRequest, ResetCustomLoginTextsToDefaultResponse, + ResetCustomPasswordChangeMessageTextToDefaultRequest, + ResetCustomPasswordChangeMessageTextToDefaultResponse, ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest, ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse, ResetCustomPasswordResetMessageTextToDefaultRequest, @@ -357,6 +367,8 @@ import { ResetLockoutPolicyToDefaultResponse, ResetLoginPolicyToDefaultRequest, ResetLoginPolicyToDefaultResponse, + ResetNotificationPolicyToDefaultRequest, + ResetNotificationPolicyToDefaultResponse, ResetPasswordAgePolicyToDefaultRequest, ResetPasswordAgePolicyToDefaultResponse, ResetPasswordComplexityPolicyToDefaultRequest, @@ -403,6 +415,8 @@ import { UpdateCustomLockoutPolicyResponse, UpdateCustomLoginPolicyRequest, UpdateCustomLoginPolicyResponse, + UpdateCustomNotificationPolicyRequest, + UpdateCustomNotificationPolicyResponse, UpdateCustomPasswordAgePolicyRequest, UpdateCustomPasswordAgePolicyResponse, UpdateCustomPasswordComplexityPolicyRequest, @@ -631,6 +645,26 @@ export class ManagementService { return this.grpcService.mgmt.getDefaultPasswordlessRegistrationMessageText(req, null).then((resp) => resp.toObject()); } + public getDefaultPasswordChangeMessageText( + req: GetDefaultPasswordChangeMessageTextRequest, + ): Promise { + return this.grpcService.mgmt.getDefaultPasswordChangeMessageText(req, null).then((resp) => resp.toObject()); + } + + public getCustomPasswordChangeMessageText( + req: GetCustomPasswordChangeMessageTextRequest, + ): Promise { + return this.grpcService.mgmt.getCustomPasswordChangeMessageText(req, null).then((resp) => resp.toObject()); + } + + public resetCustomPasswordChangeMessageTextToDefault( + lang: string, + ): Promise { + const req = new ResetCustomPasswordChangeMessageTextToDefaultRequest(); + req.setLanguage(lang); + return this.grpcService.mgmt.resetCustomPasswordChangeMessageTextToDefault(req, null).then((resp) => resp.toObject()); + } + public getCustomPasswordlessRegistrationMessageText( req: GetCustomPasswordlessRegistrationMessageTextRequest, ): Promise { @@ -1371,6 +1405,30 @@ export class ManagementService { } } + /* notification policy */ + + public getNotificationPolicy(): Promise { + const req = new GetNotificationPolicyRequest(); + return this.grpcService.mgmt.getNotificationPolicy(req, null).then((resp) => resp.toObject()); + } + + public resetNotificationPolicyToDefault(): Promise { + const req = new ResetNotificationPolicyToDefaultRequest(); + return this.grpcService.mgmt.resetNotificationPolicyToDefault(req, null).then((resp) => resp.toObject()); + } + + public addCustomNotificationPolicy( + req: AddCustomNotificationPolicyRequest, + ): Promise { + return this.grpcService.mgmt.addCustomNotificationPolicy(req, null).then((resp) => resp.toObject()); + } + + public updateCustomNotificationPolicy( + req: UpdateCustomNotificationPolicyRequest, + ): Promise { + return this.grpcService.mgmt.updateCustomNotificationPolicy(req, null).then((resp) => resp.toObject()); + } + public getUserByID(id: string): Promise { const req = new GetUserByIDRequest(); req.setId(id); diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 39b4553856..8e914388da 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -883,7 +883,7 @@ "LOGIN": "Loginverhalten und Sicherheit", "LOCKOUT": "Sperrmechanismen", "COMPLEXITY": "Passwordkomplexität", - "NOTIFICATIONS": "Benachrichtigungen", + "NOTIFICATIONS": "Benachrichtigungseinstellungen", "NOTIFICATIONS_DESC": "SMTP und SMS Einstellungen", "MESSAGETEXTS": "Benachrichtigungstexte", "IDP": "Identity Provider", @@ -1007,6 +1007,11 @@ "NUMBERERROR": "Muss eine Ziffer beinhalten.", "PATTERNERROR": "Das Passwort erfüllt nicht die vorgeschriebene Richtlinie." }, + "NOTIFICATION": { + "TITLE": "Notification", + "DESCRIPTION": "Legt fest, bei welchen Änderungen Benachrichtigungen gesendet werden", + "PASSWORDCHANGE": "Passwordänderung" + }, "PRIVATELABELING": { "TITLE": "Branding", "DESCRIPTION": "Verleihen Sie dem Login Ihren benutzerdefinierten Style und passen Sie das Verhalten an.", @@ -1146,7 +1151,8 @@ "VP": "Telefonnummerverifikation", "PR": "Passwort Wiederherstellung", "DC": "Domainbeanspruchung", - "PL": "Passwortlos" + "PL": "Passwortlos", + "PC": "Passwordwechsel" }, "CHIPS": { "firstname": "Vorname", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index f11236d038..5cc92ce7cd 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -883,7 +883,7 @@ "LOGIN": "Login Behavior and Security", "LOCKOUT": "Lockout", "COMPLEXITY": "Password complexity", - "NOTIFICATIONS": "Notification providers and SMTP", + "NOTIFICATIONS": "Notification settings", "NOTIFICATIONS_DESC": "SMTP and SMS Settings", "MESSAGETEXTS": "Message Texts", "IDP": "Identity Providers", @@ -1007,6 +1007,11 @@ "NUMBERERROR": "Must include a digit.", "PATTERNERROR": "The password does not meet the required pattern." }, + "NOTIFICATION": { + "TITLE": "Notification", + "DESCRIPTION": "Determines on which changes, notifications will be sent.", + "PASSWORDCHANGE": "Password change" + }, "PRIVATELABELING": { "TITLE": "Branding", "DESCRIPTION": "Give the login your personalized style and modify its behavior.", @@ -1146,7 +1151,8 @@ "VP": "Verify Phone", "PR": "Password Reset", "DC": "Domain Claim", - "PL": "Passwordless" + "PL": "Passwordless", + "PC": "Password Change" }, "CHIPS": { "firstname": "Firstname", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index a1589cbbd9..dfddd3fa2f 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -883,7 +883,7 @@ "LOGIN": "Comportement de connexion et sécurité", "LOCKOUT": "Verrouillage", "COMPLEXITY": "Complexité du mot de passe", - "NOTIFICATIONS": "Fournisseurs de notifications et SMTP", + "NOTIFICATIONS": "Paramètres de notification", "NOTIFICATIONS_DESC": "Paramètres SMTP et SMS", "MESSAGETEXTS": "Textes des messages", "IDP": "Fournisseurs d'identité", @@ -1007,6 +1007,11 @@ "NUMBERERROR": "Doit inclure un chiffre.", "PATTERNERROR": "Le mot de passe ne correspond pas au modèle requis." }, + "NOTIFICATION": { + "TITLE": "Notifications", + "DESCRIPTION": "Détermine sur quels changements, les notifications seront envoyées", + "PASSWORDCHANGE": "Changement de mot de passe" + }, "PRIVATELABELING": { "TITLE": "Image de marque", "DESCRIPTION": "Donnez au login votre style personnalisé et modifiez son comportement.", @@ -1146,7 +1151,8 @@ "VP": "Vérifier le téléphone", "PR": "Réinitialisation du mot de passe", "DC": "Réclamation de domaine", - "PL": "Sans mot de passe" + "PL": "Sans mot de passe", + "PC": "Changement de mot de passe" }, "CHIPS": { "firstname": "Prénom", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index b819c2a410..a2e1e506c9 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -884,7 +884,7 @@ "LOGIN": "Comportamento login e sicurezza", "LOCKOUT": "Meccanismi di bloccaggio", "COMPLEXITY": "Complessità della password", - "NOTIFICATIONS": "Notifiche", + "NOTIFICATIONS": "Impostazioni di notifica", "NOTIFICATIONS_DESC": "Impostazioni SMTP e SMS", "MESSAGETEXTS": "Testi di notifica", "IDP": "Identity Providers", @@ -1008,6 +1008,11 @@ "NUMBERERROR": "Deve includere una cifra.", "PATTERNERROR": "La password non corrisponde al modello richiesto." }, + "NOTIFICATION": { + "TITLE": "Notifiche", + "DESCRIPTION": "Determina su quali modifiche verranno inviate le notifiche", + "PASSWORDCHANGE": "Cambiamento della password" + }, "PRIVATELABELING": { "TITLE": "Branding", "DESCRIPTION": "Dai al login il tuo stile personalizzato e modifica il suo comportamento.", @@ -1147,7 +1152,8 @@ "VP": "Verificazione del telefono", "PR": "Ripristino della password", "DC": "Rivendicazione del dominio", - "PL": "Autenticazione Passwordless" + "PL": "Autenticazione Passwordless", + "PC": "Cambiamento della password" }, "CHIPS": { "firstname": "Nome", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 30b6e8e638..c5cc33f56f 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -883,7 +883,7 @@ "LOGIN": "登录行为和安全", "LOCKOUT": "安全锁策略", "COMPLEXITY": "密码复杂性", - "NOTIFICATIONS": "通知服务商", + "NOTIFICATIONS": "通知设置", "NOTIFICATIONS_DESC": "SMTP 和 SMS 设置", "MESSAGETEXTS": "消息文本", "IDP": "身份提供者", @@ -1007,6 +1007,11 @@ "NUMBERERROR": "密码必须包含数字。", "PATTERNERROR": "密码不符合要求。" }, + "NOTIFICATION": { + "TITLE": "通知", + "DESCRIPTION": "确定将发送哪些更改、通知", + "PASSWORDCHANGE": "更改密码" + }, "PRIVATELABELING": { "TITLE": "品牌标识", "DESCRIPTION": "为登录提供您的个性化风格并修改其行为。", @@ -1145,7 +1150,8 @@ "VP": "验证手机号码", "PR": "重置密码", "DC": "域名声明", - "PL": "无密码身份验证" + "PL": "无密码身份验证", + "PC": "修改密码" }, "CHIPS": { "firstname": "名", diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index ebefba59b3..c1d626054d 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -1072,6 +1072,44 @@ Variable {{.Lang}} can be set to have different links based on the language PUT: /policies/privacy +### AddNotificationPolicy + +> **rpc** AddNotificationPolicy([AddNotificationPolicyRequest](#addnotificationpolicyrequest)) +[AddNotificationPolicyResponse](#addnotificationpolicyresponse) + +Add a default notification policy for ZITADEL +it impacts all organisations without a customised policy + + + + POST: /policies/notification + + +### GetNotificationPolicy + +> **rpc** GetNotificationPolicy([GetNotificationPolicyRequest](#getnotificationpolicyrequest)) +[GetNotificationPolicyResponse](#getnotificationpolicyresponse) + +Returns the notification policy defined by the administrators of ZITADEL + + + + GET: /policies/notification + + +### UpdateNotificationPolicy + +> **rpc** UpdateNotificationPolicy([UpdateNotificationPolicyRequest](#updatenotificationpolicyrequest)) +[UpdateNotificationPolicyResponse](#updatenotificationpolicyresponse) + +Updates the default notification policy of ZITADEL +it impacts all organisations without a customised policy + + + + PUT: /policies/notification + + ### GetDefaultInitMessageText > **rpc** GetDefaultInitMessageText([GetDefaultInitMessageTextRequest](#getdefaultinitmessagetextrequest)) @@ -1104,7 +1142,7 @@ Returns the custom text for initial message (overwritten in eventstore) Sets the default custom text for initial message it impacts all organisations without customized initial message text The Following Variables can be used: -{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} @@ -1156,7 +1194,7 @@ Returns the custom text for password reset message (overwritten in eventstore) Sets the default custom text for password reset message it impacts all organisations without customized password reset message text The Following Variables can be used: -{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} @@ -1208,7 +1246,7 @@ Returns the custom text for verify email message (overwritten in eventstore) Sets the default custom text for verify email message it impacts all organisations without customized verify email message text The Following Variables can be used: -{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} @@ -1260,7 +1298,7 @@ Returns the custom text for verify phone message Sets the default custom text for verify phone message it impacts all organisations without customized verify phone message text The Following Variables can be used: -{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} @@ -1309,10 +1347,10 @@ Returns the custom text for domain claimed message (overwritten in eventstore) > **rpc** SetDefaultDomainClaimedMessageText([SetDefaultDomainClaimedMessageTextRequest](#setdefaultdomainclaimedmessagetextrequest)) [SetDefaultDomainClaimedMessageTextResponse](#setdefaultdomainclaimedmessagetextresponse) -Sets the default custom text for domain claimed phone message +Sets the default custom text for domain claimed message it impacts all organisations without customized domain claimed message text The Following Variables can be used: -{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} +{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} @@ -1364,7 +1402,7 @@ Returns the custom text for passwordless registration message (overwritten in ev Sets the default custom text for passwordless registration message it impacts all organisations without customized passwordless registration message text The Following Variables can be used: -{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} +{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} @@ -1384,6 +1422,58 @@ The default text from the translation file will trigger after DELETE: /text/message/passwordless_registration/{language} +### GetDefaultPasswordChangeMessageText + +> **rpc** GetDefaultPasswordChangeMessageText([GetDefaultPasswordChangeMessageTextRequest](#getdefaultpasswordchangemessagetextrequest)) +[GetDefaultPasswordChangeMessageTextResponse](#getdefaultpasswordchangemessagetextresponse) + +Returns the default text for password change message (translation file) + + + + GET: /text/default/message/password_change/{language} + + +### GetCustomPasswordChangeMessageText + +> **rpc** GetCustomPasswordChangeMessageText([GetCustomPasswordChangeMessageTextRequest](#getcustompasswordchangemessagetextrequest)) +[GetCustomPasswordChangeMessageTextResponse](#getcustompasswordchangemessagetextresponse) + +Returns the custom text for password change message (overwritten in eventstore) + + + + GET: /text/message/password_change/{language} + + +### SetDefaultPasswordChangeMessageText + +> **rpc** SetDefaultPasswordChangeMessageText([SetDefaultPasswordChangeMessageTextRequest](#setdefaultpasswordchangemessagetextrequest)) +[SetDefaultPasswordChangeMessageTextResponse](#setdefaultpasswordchangemessagetextresponse) + +Sets the default custom text for password change message +it impacts all organisations without customized password change message text +The Following Variables can be used: +{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} + + + + PUT: /text/message/password_change/{language} + + +### ResetCustomPasswordChangeMessageTextToDefault + +> **rpc** ResetCustomPasswordChangeMessageTextToDefault([ResetCustomPasswordChangeMessageTextToDefaultRequest](#resetcustompasswordchangemessagetexttodefaultrequest)) +[ResetCustomPasswordChangeMessageTextToDefaultResponse](#resetcustompasswordchangemessagetexttodefaultresponse) + +Removes the custom password change message text of the system +The default text from the translation file will trigger after + + + + DELETE: /text/message/password_change/{language} + + ### GetDefaultLoginTexts > **rpc** GetDefaultLoginTexts([GetDefaultLoginTextsRequest](#getdefaultlogintextsrequest)) @@ -1793,6 +1883,28 @@ This is an empty request +### AddNotificationPolicyRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| password_change | bool | - | | + + + + +### AddNotificationPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### AddOIDCIDPRequest @@ -2210,6 +2322,28 @@ This is an empty request +### GetCustomPasswordChangeMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetCustomPasswordChangeMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + ### GetCustomPasswordResetMessageTextRequest @@ -2398,6 +2532,28 @@ This is an empty request +### GetDefaultPasswordChangeMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetDefaultPasswordChangeMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + ### GetDefaultPasswordResetMessageTextRequest @@ -2627,6 +2783,23 @@ This is an empty request +### GetNotificationPolicyRequest +This is an empty request + + + + +### GetNotificationPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| policy | zitadel.policy.v1.NotificationPolicy | - | | + + + + ### GetOIDCSettingsRequest This is an empty request @@ -3839,6 +4012,28 @@ this is en empty request +### ResetCustomPasswordChangeMessageTextToDefaultRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### ResetCustomPasswordChangeMessageTextToDefaultResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### ResetCustomPasswordResetMessageTextToDefaultRequest @@ -4085,6 +4280,35 @@ this is en empty request +### SetDefaultPasswordChangeMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 800
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetDefaultPasswordChangeMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### SetDefaultPasswordResetMessageTextRequest @@ -4581,6 +4805,28 @@ this is en empty request +### UpdateNotificationPolicyRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| password_change | bool | - | | + + + + +### UpdateNotificationPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### UpdateOIDCSettingsRequest diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index e2341425a8..ae987b1f2f 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -2231,7 +2231,7 @@ Variable {{.Lang}} can be set to have different links based on the language > **rpc** UpdateCustomPrivacyPolicy([UpdateCustomPrivacyPolicyRequest](#updatecustomprivacypolicyrequest)) [UpdateCustomPrivacyPolicyResponse](#updatecustomprivacypolicyresponse) -Update the privacy complexity policy for the organisation +Update the privacy policy for the organisation With this policy privacy relevant things can be configured (e.g. tos link) Variable {{.Lang}} can be set to have different links based on the language @@ -2253,6 +2253,71 @@ The default policy of the IAM will trigger after DELETE: /policies/privacy +### GetNotificationPolicy + +> **rpc** GetNotificationPolicy([GetNotificationPolicyRequest](#getnotificationpolicyrequest)) +[GetNotificationPolicyResponse](#getnotificationpolicyresponse) + +Returns the notification policy of the organisation +With this notification policy it can be configured how users should be notified + + + + GET: /policies/notification + + +### GetDefaultNotificationPolicy + +> **rpc** GetDefaultNotificationPolicy([GetDefaultNotificationPolicyRequest](#getdefaultnotificationpolicyrequest)) +[GetDefaultNotificationPolicyResponse](#getdefaultnotificationpolicyresponse) + +Returns the default notification policy of the IAM +With this notification privacy it can be configured how users should be notified + + + + GET: /policies/default/notification + + +### AddCustomNotificationPolicy + +> **rpc** AddCustomNotificationPolicy([AddCustomNotificationPolicyRequest](#addcustomnotificationpolicyrequest)) +[AddCustomNotificationPolicyResponse](#addcustomnotificationpolicyresponse) + +Add a custom notification policy for the organisation +With this notification privacy it can be configured how users should be notified + + + + POST: /policies/notification + + +### UpdateCustomNotificationPolicy + +> **rpc** UpdateCustomNotificationPolicy([UpdateCustomNotificationPolicyRequest](#updatecustomnotificationpolicyrequest)) +[UpdateCustomNotificationPolicyResponse](#updatecustomnotificationpolicyresponse) + +Update the notification policy for the organisation +With this notification privacy it can be configured how users should be notified + + + + PUT: /policies/notification + + +### ResetNotificationPolicyToDefault + +> **rpc** ResetNotificationPolicyToDefault([ResetNotificationPolicyToDefaultRequest](#resetnotificationpolicytodefaultrequest)) +[ResetNotificationPolicyToDefaultResponse](#resetnotificationpolicytodefaultresponse) + +Removes the notification policy of the organisation +The default policy of the IAM will trigger after + + + + DELETE: /policies/notification + + ### GetLabelPolicy > **rpc** GetLabelPolicy([GetLabelPolicyRequest](#getlabelpolicyrequest)) @@ -2485,7 +2550,7 @@ Returns the default text for password reset message Sets the custom text for password reset message The Following Variables can be used: -{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} @@ -2536,7 +2601,7 @@ Returns the default text for verify email message Sets the custom text for verify email message The Following Variables can be used: -{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} @@ -2587,7 +2652,7 @@ Returns the custom text for verify email message Sets the default custom text for verify email message The Following Variables can be used: -{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} +{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} @@ -2638,7 +2703,7 @@ Returns the custom text for domain claimed message Sets the custom text for domain claimed message The Following Variables can be used: -{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} +{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} @@ -2689,7 +2754,7 @@ Returns the custom text for passwordless link message Sets the custom text for passwordless link message The Following Variables can be used: -{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} +{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} @@ -2709,6 +2774,57 @@ The default text of the IAM will trigger after DELETE: /text/message/passwordless_registration/{language} +### GetCustomPasswordChangeMessageText + +> **rpc** GetCustomPasswordChangeMessageText([GetCustomPasswordChangeMessageTextRequest](#getcustompasswordchangemessagetextrequest)) +[GetCustomPasswordChangeMessageTextResponse](#getcustompasswordchangemessagetextresponse) + +Returns the custom text for password change message + + + + GET: /text/message/password_change/{language} + + +### GetDefaultPasswordChangeMessageText + +> **rpc** GetDefaultPasswordChangeMessageText([GetDefaultPasswordChangeMessageTextRequest](#getdefaultpasswordchangemessagetextrequest)) +[GetDefaultPasswordChangeMessageTextResponse](#getdefaultpasswordchangemessagetextresponse) + +Returns the custom text for password change link message + + + + GET: /text/default/message/password_change/{language} + + +### SetCustomPasswordChangeMessageCustomText + +> **rpc** SetCustomPasswordChangeMessageCustomText([SetCustomPasswordChangeMessageTextRequest](#setcustompasswordchangemessagetextrequest)) +[SetCustomPasswordChangeMessageTextResponse](#setcustompasswordchangemessagetextresponse) + +Sets the custom text for password change message +The Following Variables can be used: +{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} + + + + PUT: /text/message/password_change/{language} + + +### ResetCustomPasswordChangeMessageTextToDefault + +> **rpc** ResetCustomPasswordChangeMessageTextToDefault([ResetCustomPasswordChangeMessageTextToDefaultRequest](#resetcustompasswordchangemessagetexttodefaultrequest)) +[ResetCustomPasswordChangeMessageTextToDefaultResponse](#resetcustompasswordchangemessagetexttodefaultresponse) + +Removes the custom password change message text of the organisation +The default text of the IAM will trigger after + + + + DELETE: /text/message/password_change/{language} + + ### GetCustomLoginTexts > **rpc** GetCustomLoginTexts([GetCustomLoginTextsRequest](#getcustomlogintextsrequest)) @@ -3226,6 +3342,28 @@ This is an empty request +### AddCustomNotificationPolicyRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| password_change | bool | - | | + + + + +### AddCustomNotificationPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### AddCustomPasswordAgePolicyRequest @@ -4446,6 +4584,28 @@ This is an empty request +### GetCustomPasswordChangeMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetCustomPasswordChangeMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + ### GetCustomPasswordResetMessageTextRequest @@ -4651,6 +4811,23 @@ This is an empty request +### GetDefaultNotificationPolicyRequest +This is an empty request + + + + +### GetDefaultNotificationPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| policy | zitadel.policy.v1.NotificationPolicy | - | | + + + + ### GetDefaultPasswordAgePolicyRequest This is an empty request @@ -4668,6 +4845,28 @@ This is an empty request +### GetDefaultPasswordChangeMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### GetDefaultPasswordChangeMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| custom_text | zitadel.text.v1.MessageCustomText | - | | + + + + ### GetDefaultPasswordComplexityPolicyRequest This is an empty request @@ -5034,6 +5233,23 @@ This is an empty request +### GetNotificationPolicyRequest +This is an empty request + + + + +### GetNotificationPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| policy | zitadel.policy.v1.NotificationPolicy | - | | + + + + ### GetOIDCInformationRequest This is an empty request @@ -7442,6 +7658,28 @@ This is an empty request +### ResetCustomPasswordChangeMessageTextToDefaultRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### ResetCustomPasswordChangeMessageTextToDefaultResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### ResetCustomPasswordResetMessageTextToDefaultRequest @@ -7581,6 +7819,23 @@ This is an empty request +### ResetNotificationPolicyToDefaultRequest +This is an empty request + + + + +### ResetNotificationPolicyToDefaultResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### ResetPasswordAgePolicyToDefaultRequest This is an empty request @@ -7791,6 +8046,35 @@ This is an empty request +### SetCustomPasswordChangeMessageTextRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 200
| +| title | string | - | string.max_len: 200
| +| pre_header | string | - | string.max_len: 200
| +| subject | string | - | string.max_len: 200
| +| greeting | string | - | string.max_len: 200
| +| text | string | - | string.max_len: 800
| +| button_text | string | - | string.max_len: 200
| +| footer_text | string | - | string.max_len: 200
| + + + + +### SetCustomPasswordChangeMessageTextResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### SetCustomPasswordResetMessageTextRequest @@ -8234,6 +8518,28 @@ This is an empty request +### UpdateCustomNotificationPolicyRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| password_change | bool | - | | + + + + +### UpdateCustomNotificationPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### UpdateCustomPasswordAgePolicyRequest diff --git a/docs/docs/apis/proto/policy.md b/docs/docs/apis/proto/policy.md index 8af9ffbda5..ff5b6fb044 100644 --- a/docs/docs/apis/proto/policy.md +++ b/docs/docs/apis/proto/policy.md @@ -95,6 +95,19 @@ title: zitadel/policy.proto +### NotificationPolicy + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | +| is_default | bool | - | | +| password_change | bool | - | | + + + + ### OrgIAMPolicy deprecated: please use DomainPolicy instead diff --git a/docs/docs/guides/manage/console/instance-settings.mdx b/docs/docs/guides/manage/console/instance-settings.mdx index 0bd34132f0..1d131b4795 100644 --- a/docs/docs/guides/manage/console/instance-settings.mdx +++ b/docs/docs/guides/manage/console/instance-settings.mdx @@ -15,7 +15,7 @@ To access instance settings, use the instance page at `{instanceDomain}/ui/conso When you configure your instance, you can set the following: - **General**: Default Language for the UI -- [**Notification providers and SMTP**](#notification-providers-and-smtp): Email Server settings, so initialization-, verification- and other mails are sent from your own domain. For SMS, Twilio is supported as notification provider. +- [**Notification settings**](#notification-providers-and-smtp): Notification and Email Server settings, so initialization-, verification- and other mails are sent from your own domain. For SMS, Twilio is supported as notification provider. - [**Login Behaviour and Access**](#login-behaviour-and-access): Multifactor Authentication Options and Enforcement, Define whether Passwordless authentication methods are allowed or not, Set Login Lifetimes and advanced behavour for the login interface. - [**Identity Providers**](#identity-providers): Define IDPs which are available for all organizations - [**Password Complexity**](#password-complexity): Requirements for Passwords ex. Symbols, Numbers, min length and more. @@ -48,9 +48,16 @@ Make sure you click the "Apply configuration" button after you finish your confi Branding settings applied on you instance act as a default for all your organizations. If you need custom branding on a organization take a look at our guide under [organization settiong](./organizations#branding). -## Notification providers and SMTP +## Notification settings -In the notification settings you can configure your SMTP Server settings and your SMS Provider. At the moment Twilio is available as SMS provider. +In the notification settings you can configure when to notify users about certain events and you can customize your SMTP Server settings and your SMS Provider. +At the moment Twilio is available as SMS provider. + +### Notification + +You can configure on which changes the users will be notified. The text of the message can be changed in the [Message texts](#message-texts) + +Notification ### SMTP @@ -197,13 +204,14 @@ Example: These are the texts for your notification mails. Available for change are: -| Message Text | Description | -| -------------- | ---------------------------------------------------------------------------------------------------------------- | -| Domain Claim | Enable self register possibility in the login ui | -| Initialization | The mail after a user has been created. A code is part of the message which then must be verified on first login | -| Passwordless | Possibility to login with an external identity (e.g Google, Microsoft, Apple, etc) | -| Password Reset | Force a user to register and use a multifactor authentication | -| Verify Email | Choose if passwordless login is allowed or not | +| Message Text | Description | +| --------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Domain Claim | The Mail after an organisation claimed a domain for itself. Users on other organisations with this domain will be notified | +| Initialization | The mail after a user has been created. A code is part of the message which then must be verified on first login | +| Passwordless | The Mail to register an additional passwordless device by a link | +| Password Reset | The Mail to reset the password by a link | +| Verify Email | The mail after the email has been changed. A code is part of the message which then must be verified on the next login | +| Password Change | Notify the user, that the password has been changed. Can be configured in [Notification](#notification) | You can set the locale of the translations on the right. diff --git a/docs/static/img/guides/console/notification.png b/docs/static/img/guides/console/notification.png new file mode 100644 index 0000000000000000000000000000000000000000..10299c635ace3f2a88b757dbb9109d0d897a0b4a GIT binary patch literal 9619 zcmch7c{H1C*Kb>`p~grJMX6A%8Zjf3oge)4lI~{r3LtYws(@*hrW4Jm2|K zr%tiz-POK->eOiz?Rkiip7zWBLHgmTQ&%kYv~NEQw8duyARp>99B&?z4m{9}!A<3E zcj}$dB}gE>DZJ0kzpr6g%2_7=BjU8S#y3PB^ZD33#9eNm$A)fg@u#(IUKzI>4Dav# z8djPPb#92>kRz6@9HHTCNcKU@n}Ru+!r03{vj3S5rl&|EwhgXGXe zOo0GEO~$uYq7Isj!rpP(28_bmo^KIM!jXSTbIQ^t)a!u6Xw&NMVoxS<{~MF;a{;Ve zcN3;Pg9Yc6OIvh6*O#H;>SIL-?Ik*0hPCiI_WB`w@Nd}|#WCL!&+>rT57YZy;thKJ z27X03p?aXc*yXyC(#;;mRUIR?#T-dZc)!CZ!rk{Fr=+sqK$oSGd{ZVlhk=SMsWjn{Q{~g zc;{Lq^n=pmrJ$)*&p3QT~8U;=`qNM&oe}e=7OjBi6aU8Gb zVg$SRAn)=ufb4eH^gy^VsQf`%*3@4nj+nbqb2*`VQ7y~q--kFyOBvZzac9GiZvq^h zlmh?QY^<$&-ow(iN5ZHC!MlM@{-IYu=aYgMBZ4tF;$0xhLCWImupM#K(MTU###oM< zo}Gmlp6p$9d~xj9N>2Fdav!BT+_qsyd^Cud1$6D}0Ck>!wnqflA24&HoqAI1qnKL) zz%APi3I`S$P3(%@3E>WTR+uL-{R```=G|BHluN^bZx-flN6D(646;_KNCCqgFIaC- ziI?GDF-2K95|W^UpHooGskW&k2%N|Exnc6nNqlu3cqL#~dWYfxIoi(;KK!m1=BJ~| zV0Z^|Xr5~$(KrpX7h&})=(P)x1)I>Ddcj(V&>S#+dt`JLO#o*5my}M|>oo}S*ewy! zi(l76tc;<15?4vVo1wpJR~uYFFdAf49&w?d5XzL0wZ=CuyD+8x91QbN2D1*!kPxuC zUMG&5OI?Je7JBpP7D6)+=gB8CTy*gUhVbUBQ!B*tFxjNrS78suWZ12=A_U`^|I7|@ z@Z72l4#`;e!SS6a*VS$Y1wLX z-^&!dFRdmiBLGwg+wa>PB@$4G({M3)icpCA9G`}}S1tmj;FPE(%H2%dU_gKGHwxhQ zooP0PY16^4fX_~1&(=BCQA3p5#EmlM+iinFjpk;=wp>BA)E|@!+Xob+F7sUJMDy_x z4aw*G+S)0Do~C!Qqeeoqd4-U2d;tW*%JiKkPo$hnMy#}UOPpxiTz;bOo8eV zZS=7dDAiwC))4zm%-i6dJ(|f$!U0JSXEe7_AE^nq5ml!oG_bu~&;HlIOu!9QIPq(tKTB+0SASJ%hk=Oj0{Mwm&mw z>;V+APR+d=h1nxRLR6P;F}oQ89CNxevjU|$J?f4rADJYJWF3V9sIW`-#};VomL?u% z=w=GymQMLdg`F&^Hn;|&pL+uPq0J)PW-nMJLr2Was;%N;sQ!d_nWZh|+)bB={t%b6 z;&VaBEC%tGjYn`sM)5i<(TypY=9ni$B(<^MOdl>ARPLAQs9;)?wqK8K_3$f-1@wk+ z{|NO;N(k?!wC693aL7&;Exe!?=akNj5YHF7xCy%vSG2VQ^ zPUTyY>&V1qLu{h@oPp45=I{cfkky@duh|fl7LMyQeyG+pMIzvX1HJBOuaS|W*|I3F zz2`GQBsCKL@LZG(#WuCo1TK(#)Rhym2AM1_oI*{-L(j9cr|7eI8njb*AKN6 zzJObgqIpcUJtYiB;wm7w>XAhT;`aU(kMh>VXTeSdOK}6KrZl>Fnp7$SpfkNydoRW? zx>QwBP*C}-G@b!&kwVbtTSks~)Z911awN+f?kdde%n{PEEQC2-Mh_@A87IpkUULn4 zK}*=#OdM|o8Rm3urboHjlS@~a4+v>OM^`XN#OY6CTLX-)pZO(QWwM$W-*ciID2X%< zG%E%j{Myg7*!WSkGgQAm@9LY%AyM2w7&Uln;V3#k z5h4N*9P~(vu%_{`UStu9=ot=gD#xh%knj6WJ$1_&{n04&1_4~MpV|l>ko7DaQ80=m z8uV|#(^bfka=_6V`XGKc_@oEM)ohlhZep(g%Mr)gJ!?`Zn?5#X`2iPzU*SkkfpU;Fl+4B$xoIW9pyE#$Pwd))HV}7&R`>~5ZyK=Vul=iSyKsY{Xg>S8&n9$Bj5AxW- zy}uZPzInoFMVoso5kPp~IRdD{jXd=p0W{R(hmP8Ym%v(t+c%{nJsKV zrGXL(Q@$U<(V;L*pW4>%c263JLihjZc2c-3Lu{^g#$Vd3h-)_!4VaPZhR#z21ARf| zO~yvpz#-ATu`V&u`N21ck=NA(-_H6pu?bjPa&pMnqtTf^;$@0C&$gy;JU0E3WO=5Z zN%OeL>)d5yLhsA~`s-S|8jp0`cm3o&IFBf|-?onCPvtd zIPFsTCl{gNf40J#)h_nKt!uu$?^JUD^r)bob{xII#G0q7KV-IigndCpU47s}4xD5V zp1u??v+}7Qi5J(KheB33h3X7P)uu&~)aQnXo$_;ETryaSBaWZveNc3bZAsjZzLVrb zBbb>kkCq+8_u@$8k1GakpE2o z(ZH~4RmjEc=VXf<$TBx>(!Vdf?675~pRR7-ik{8qxXE(i-upd`0gse>2iG_JJo2U= zSCt(v&W6eKy&GM0DQcueN}oAkyJ{N+J|W5zbQ{f^typ!2ptq?bU!zR8yOs7(pFkbg zGUVsvNK5#TF$5*VW(zX&dlPf;BTDtJfPsO=^P_SX*&5vn!kdTR%4=c9bY%dI^BG}L zE4+2~Ed8SHP}}%lf^&aR_Fac;r(YR1KMH6 z#o0^C1>QuijFC#(d^(ku5%Dq-6`UWb@IUETUFqg!hkmy-&z^!Q&utFalSC)twV2;0 zuzi30iS;vZ^hWNx&NXHOoB?+&`-;V=SNF#-PN!?8H6r2g#4PRW4k6Mc153mz|=Q{teYy&GFKRZ{>ctm7WY~E@uWvI ztMCZdw7a3+BArqktK7}n)`JNeS+fL%qS{R@)ZRRLmMRY=DD&L(O=- zaO0J;cBFEe8w!UZl-lm8zS78PYUu9^%S=aCrnSjAvVX60YA0R}k4#vfNBIxE=v+}> z%hf&faa5hty@1+!)7dF=W|$bO-h60U`{>u&`}%395)qWc?(-sQJWiTaBV(&x7LU4{ zLX1WtT0>bEUavSWJlkncBHu*Za&p1gjyL9PM#Kw~ku`5k%KzSW$FC2CiWy812n&&) ziZ^fMT*TS)37&ce3p>`xFDz+tmHBDO8}Xt#^Lf?nemaC1$TzUOrb$VV#Wj6Qnia@t z@0=aWkUA0((_U`R0qn%qc28tcUAiyMd{B6xd@%>WFz8DiIx-x@4yYF}&z`;7*yK7_ zN8UDdat%xtk-{5(IW9{8Ml==5-5%qYTN00t5ib=XIlf{e+Uwj^%y9%bp6P;4c95dI zJ$OGkf$q6MMck+hRCBwdoE+U$Rr2f&EXAXPeMeR-SUqihsPsBQ_v{BBZw38vcZ0{D zs%~OZCaF@rlzhSkXf=Pp_*DW97nFe*k9-O)kj3m0HC@Au6yD8Z}&}X?8t4)#+{YCTwawDS7y#}fu9*Z`L~XRkL=NS-TTKc zBXsxsqXmP!g|kyY_6W;Bn=j9u)04j@>V+rmJvBNT-180B+KuS9Amo-@e$vEo(?v+t4{k53IaSFCU4Bq9`Fp7#+_n40Giy+_2>R#NErW z?4q${ioG1$n}e0a@lamwAz9w^LE~ntO(|=jw}0E zwc{G>8A${IE9eLRx-Y-+eUYb=h(km7U)@nB1jdR_NP(Ppe~(88MaisIb+;U@g+-|m z)+y}Ua0~5TBh2ehu*##y$>9^o+BBIDhgG#Il9vZ%SAFcZYrMC--AG1O9nOU43zI;( zxe3%{SQKIQNjaq+Dy#v|?pRBAGU`?qBXos+7JTXlaQymY0MKt$Anj!GoZj#*y6v8u z3}4O_zmQ9j%hNYjnQts_C9=Ehh+h1E9 zsB0-7-T0F6Mbu63adPC#H*#};v>K=!%2E9*4_HozTSR~SuQ;_zne3H$ol^T05Bo)6 zQc+-D&5AKCQ0H-SXkkqL6-qjuE`r4%#&5l@iB7*h)Ok}3HW(?1l)`&*U+O$Zjo67C zdOh4OB~mNSQ)iSK^YYbafqx6(Z$6|M*BJzO%N@ z-+d|)#U7b6qLB$@bParC&kM^`y^Pz2KbMgIBV5*QW`&ipBBZN**gxuctl@2FE#9ag zslY7hY!tuF5B(<^~ap7}N{vlV8GZrt{wnrEA zGrJ&H2>8QV1AR#wwez3W6-Di^RYmhqb7%V^Lir{j&CLS#YE`{@7G0u8X!e)sR&F|v zba0(k^e*IVURdHU4PS2gbqnFhi0C?DF*lhr%I6J(&qGu+mvh6&yp|8SRt1l5f1wtt&wqk2L=3N!?whM6P3Gfk@m!en zqq|xvGzdbDxKg}#r4+=SE+xB${zX>9Ul{e#F?j@F&P6G;rtZaELUN@u1|d?jRn6~M z@zEp3J;y6h3l#>-{=BE3KIC7N{}Y5Ds0}Hr9!y?UkpWaa%4aPI9KX~&(#8JaN79>U ze-|-1dJrMA_$Kk1%-rL;#2#jWqsL#*uk)RUqR6S7vXv1R=b(^r`jGu~R*i;z#;^>8 za4$DBII3lyGgXDA<;$Fm>APGRqTQC0WiQ4RVtQ)tD^Eq|*Y~U(z7%CTneobF0o!ExZ1=oE=sWt;twQS2 z=GYfU&%`V~vQM5CofJAdafWzfX{qB^`O4Y+Lex+=!93s2{tjCg04Qe1g$fz$^p$WZ zoKVAbR<^VFKkJtfW=s0r%ziv`kTnfA6J7y{dh>?zXu9Q5Xqz09rS_yc_A9&9TO{k@o)8-ql}h zy9T#<4DFa&M24-AAFFBuSU^C*8v1O>2nBiJ)U_c4F#|S~x$Mu5PgSYkm^!u!Yz@8; zhB?u86GtrexSCG!B~1yJFJE8FB;H$kl<0uB1a&~tit{xe!=-2f@0@i8e*`f;y^?AJQz|~PKnF!Q;uS*+s(Hve` z_u9*zuOsC3YUTaYa+VF67c~Eo8s_~sQp5kOmiuMF;40RlMavAEx7Vf%0VDEo>_vHc71>5 zpDh#Yv>`vO2NKPpmak3{?b!1)o&Mj+tq+!Kmr}mhp&_<1ePuy=!_KX@toc{OX`fbG zEeUXoc@d?_{ja}qQD5q@2Ze;_-CE-D=RU;3q;e>k-Or5FBn_}lj7U`OnZC6R7C0=Svu$K_VF?&%7;VMKgLz*@c8`}jt} zFMzE?f773aHnn5#{Z)0`oNmtbNc%zUC(cb<#iY%K(Vyy&ZjCTzp0G{wjIXXR_4i!< z)O?5jlyFxjo|ZkINF#Nm&2GVu>+tFuSpl=)_f8vKIR_LjAZk~Bx^Fh^&}lHR=HJ+! zsUzRChBYVHyqwWVe^{G+AOl44jIqCif%W!$26{#a;M$ z_IGym?BT&I7xq4*38Y#dVC9rX%)t3M0V@9qTkv|Oh zI?Xm~XwkM*0**U~n(OF|DfRS9E>cfJFmFawnvWHl?UC#qJJlXBN{-Ov)wt;jiJPs| z4-h)WVmm90LsY_FR)iOo?OiLVpPx9b&MGb;ZNO+EL=#x60PQ02Wa(^aJ?J$f(lw#( zu&3OW?Bq%dM&*L*=K_-ap+*_Y;y=L6huvKXeALK(IuZ?1=n^uEl(pHlM2r_>CsPq= zCnpJl=LEZ#engAscnlunh~Ur+G+XJOmp}lx)+|cmcR5}GtrsW@sPfM7Hp;iSDTY^9 z@jotM*K(IGec0&qeIfm7f+UJLc>1^53nZA9m}?&IN)}4=zahmU8`mQp`B@r&AL}68 z1A5%Yg$jg9MN+Sr>KP<53Y64%-JmzHDxGrYie@Ua7FfPL#SCuRKyh`IR5Y!g+db5$ zkoRX4Zk~t20d6;kJZ5Up2>bH(OFr8=hHc{&#iKLLrZ>;Z!oX}7-1^=0zL@dkS~P(>yF`h;^@sNih~M!>OZArMT5e z1(|1TP=q$mNnh_uvkSwo_cPxX5Rd!}nW#aezo7x?SsX8@{Yei~o5C!)sO9x{UZ0C4 z@u&sy`X)BjpPQ>PU*3Isn7O3(Cy^3riwxf3ZsvKbu$4lY4ZP+Ckf4yiWkq>1| z=5g9~88WBNJpLrlW;!MBTL3wHp%0X0dv!Ta(G-qrPYCyp<;l(3Qv~gvNcODG!6||G znCn?0#=Pw7cHR-zu^4O5-SIGX{4Jg2d!I(nXtA{`C);9+&CtrU<3sLw6>8%!aTSud zs^5z9)ESBo^-1qN1d~F17q08*;TOf8Nj83Y08<#G;%>6n) z^Kky2wUDqj-qj3b3Wc2NEsoV`w9>r2~qf%WS2lW_e@hpljDEClKj7f3;NTDvCh`; zb*_^%{0o2V^FhvQ+^F3~+{W>$qqW8fZL|IlT6Tj~H(9}m8!?*aX+5sKf2vDHPY7AF z15`W6mxb|oP;yfBw$PCKo2O~b^p*)H)o{&QX7;qaa71EO03j-lS&ByOkINiU5vEwO z3DYB>tcHz+7|8hsn6|78L6g6@{)}AlHHQ~j$*;V(BSqd{u%;E(DRitF%mb0n@Gm=P zkqm7G1XtAmlZtz)DF@1LQV|{+#i2PIdo9*{LG*Y%h3~+LzkThBEKn0c&OHy>gApw2 z*H=Aj@;Ch5vSi0k;s*J1etDk5CJwZ)WcN0$FdTXGfz67-q@KM$d2KjF`Pi!W9UerB z33k7jV`XY;#81n8mh(%aRi(7?pUtshEel^_n3|h3rmsAq@xKafA4ZZeDzf!;JfA6G zGs`YL+DGTf)1o5}0*9{YXBE(=CYrWCvvS3mtD@=@!fNg_q+g)bBJbSAUR&fjxqtBa zA}8wn$$bQE{2Rn0#EG_^MQfL8^640epN!y%F30;6}I8$csRMgE?A z$?u9oyIAOas&v3P(!pVe=W$qt=->$OKRZPn#M#mpqSmS4Fkha8kV)3`eUw$TZ+c@G zjv=OcH}dbd>C*6p=4svm)e@rQIN`#Veq^OGmR$MQ5+X&R%R27Afj&UE*E&$BUWkXf zICNO;UJr%tk20qS52vJE%{x2>92-;)62sB37Q6Y1_J>N8=>v7uA1#F^ zR*~mFoPhA~8TXk9K~P5LbEbwWLh%iDl)R+LKd)B9!f`Jz<}^CeH_hoV*KM+b8>3yq z=OCMYRDE@6$(lllbO0;_GeG@9V)hJ*@am&Jd#mEPB@nQ-rO}NrHV(}%Lsez0+yBlY6Jbz4We)yO|yMI#6x^@R!yRmCM-)N6}DQvCAXX=P1fcuEE z!t2jRh=$s|rp`k%hrf<%fC;tzwF@a#KvD_!zjTk>KdK2#KQ13TM{2^`hP1%B{2oLY zPpd3FI8IlqGz4d_IycMBQVg+=XI^!-^XrFM+A6jp0#TZdPOpOq-;OHK-+h|l?0sYq z{D0Doe_;Ew^C;Qq{rDpbcMg%WKc;$wLRQ#ek)U6}y}|09Q_RGS#o$gv-T5LuxB%a( z(%LkZY8CrWv><;HFs?d%;YiM&(4mM5= z3@CYX2cY%sq#2d>_MgDlr8%iz7n6a&;NX`H`hNq4W=kbPL1qr#Ft9Pn?r-~YrnO0K zdfofGE#Tx-Hk=r%gG)PZGHtX@FmYK+L{KN<%)fe^Tnf3)6*mSpwVb4mX4p*e=}x61 z|NL#~4+1V#@SH#eg$VMoY>!<$ruzHUZTpA&l-x4d@*q?)oH&ZN4HMT&f4)Z!UZ*)< z%dA+PUg-SGH`JAiy6IA_UG8*VTPu@0z8G g|Mi`vrDM9US`O`eu@D>DWvf$qI!4;%nvY-o2SX&SFaQ7m literal 0 HcmV?d00001 diff --git a/internal/api/grpc/admin/custom_text.go b/internal/api/grpc/admin/custom_text.go index 187318659f..0baa2d537b 100644 --- a/internal/api/grpc/admin/custom_text.go +++ b/internal/api/grpc/admin/custom_text.go @@ -252,6 +252,54 @@ func (s *Server) ResetCustomDomainClaimedMessageTextToDefault(ctx context.Contex }, nil } +func (s *Server) GetDefaultPasswordChangeMessageText(ctx context.Context, req *admin_pb.GetDefaultPasswordChangeMessageTextRequest) (*admin_pb.GetDefaultPasswordChangeMessageTextResponse, error) { + msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.PasswordChangeMessageType, req.Language) + if err != nil { + return nil, err + } + return &admin_pb.GetDefaultPasswordChangeMessageTextResponse{ + CustomText: text_grpc.ModelCustomMessageTextToPb(msg), + }, nil +} + +func (s *Server) GetCustomPasswordChangeMessageText(ctx context.Context, req *admin_pb.GetCustomPasswordChangeMessageTextRequest) (*admin_pb.GetCustomPasswordChangeMessageTextResponse, error) { + msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.PasswordChangeMessageType, req.Language, false) + if err != nil { + return nil, err + } + return &admin_pb.GetCustomPasswordChangeMessageTextResponse{ + CustomText: text_grpc.ModelCustomMessageTextToPb(msg), + }, nil +} + +func (s *Server) SetDefaultPasswordChangeMessageText(ctx context.Context, req *admin_pb.SetDefaultPasswordChangeMessageTextRequest) (*admin_pb.SetDefaultPasswordChangeMessageTextResponse, error) { + result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetPasswordChangeCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &admin_pb.SetDefaultPasswordChangeMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) ResetCustomPasswordChangeMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomPasswordChangeMessageTextToDefaultRequest) (*admin_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse, error) { + result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.PasswordChangeMessageType, language.Make(req.Language)) + if err != nil { + return nil, err + } + return &admin_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + func (s *Server) GetDefaultPasswordlessRegistrationMessageText(ctx context.Context, req *admin_pb.GetDefaultPasswordlessRegistrationMessageTextRequest) (*admin_pb.GetDefaultPasswordlessRegistrationMessageTextResponse, error) { msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.PasswordlessRegistrationMessageType, req.Language) if err != nil { diff --git a/internal/api/grpc/admin/custom_text_converter.go b/internal/api/grpc/admin/custom_text_converter.go index 3622243765..adcacb4f3b 100644 --- a/internal/api/grpc/admin/custom_text_converter.go +++ b/internal/api/grpc/admin/custom_text_converter.go @@ -83,6 +83,21 @@ func SetDomainClaimedCustomTextToDomain(msg *admin_pb.SetDefaultDomainClaimedMes } } +func SetPasswordChangeCustomTextToDomain(msg *admin_pb.SetDefaultPasswordChangeMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.PasswordChangeMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} + func SetPasswordlessRegistrationCustomTextToDomain(msg *admin_pb.SetDefaultPasswordlessRegistrationMessageTextRequest) *domain.CustomMessageText { langTag := language.Make(msg.Language) return &domain.CustomMessageText{ diff --git a/internal/api/grpc/admin/notification_policy.go b/internal/api/grpc/admin/notification_policy.go new file mode 100644 index 0000000000..d6f8f94186 --- /dev/null +++ b/internal/api/grpc/admin/notification_policy.go @@ -0,0 +1,46 @@ +package admin + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/object" + policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy" + admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" +) + +func (s *Server) AddNotificationPolicy(ctx context.Context, req *admin_pb.AddNotificationPolicyRequest) (*admin_pb.AddNotificationPolicyResponse, error) { + result, err := s.command.AddDefaultNotificationPolicy(ctx, authz.GetInstance(ctx).InstanceID(), req.GetPasswordChange()) + if err != nil { + return nil, err + } + return &admin_pb.AddNotificationPolicyResponse{ + Details: object.AddToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) GetNotificationPolicy(ctx context.Context, _ *admin_pb.GetNotificationPolicyRequest) (*admin_pb.GetNotificationPolicyResponse, error) { + policy, err := s.query.DefaultNotificationPolicy(ctx, true) + if err != nil { + return nil, err + } + return &admin_pb.GetNotificationPolicyResponse{Policy: policy_grpc.ModelNotificationPolicyToPb(policy)}, nil +} + +func (s *Server) UpdateNotificationPolicy(ctx context.Context, req *admin_pb.UpdateNotificationPolicyRequest) (*admin_pb.UpdateNotificationPolicyResponse, error) { + result, err := s.command.ChangeDefaultNotificationPolicy(ctx, authz.GetInstance(ctx).InstanceID(), req.GetPasswordChange()) + if err != nil { + return nil, err + } + return &admin_pb.UpdateNotificationPolicyResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} diff --git a/internal/api/grpc/management/custom_text.go b/internal/api/grpc/management/custom_text.go index 6473c7813b..d7929e2ced 100644 --- a/internal/api/grpc/management/custom_text.go +++ b/internal/api/grpc/management/custom_text.go @@ -252,6 +252,54 @@ func (s *Server) ResetCustomDomainClaimedMessageTextToDefault(ctx context.Contex }, nil } +func (s *Server) GetCustomPasswordChangeMessageText(ctx context.Context, req *mgmt_pb.GetCustomPasswordChangeMessageTextRequest) (*mgmt_pb.GetCustomPasswordChangeMessageTextResponse, error) { + msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordChangeMessageType, req.Language, false) + if err != nil { + return nil, err + } + return &mgmt_pb.GetCustomPasswordChangeMessageTextResponse{ + CustomText: text_grpc.ModelCustomMessageTextToPb(msg), + }, nil +} + +func (s *Server) GetDefaultPasswordChangeMessageText(ctx context.Context, req *mgmt_pb.GetDefaultPasswordChangeMessageTextRequest) (*mgmt_pb.GetDefaultPasswordChangeMessageTextResponse, error) { + msg, err := s.query.IAMMessageTextByTypeAndLanguage(ctx, domain.PasswordChangeMessageType, req.Language) + if err != nil { + return nil, err + } + return &mgmt_pb.GetDefaultPasswordChangeMessageTextResponse{ + CustomText: text_grpc.ModelCustomMessageTextToPb(msg), + }, nil +} + +func (s *Server) SetCustomPasswordChangeMessageCustomText(ctx context.Context, req *mgmt_pb.SetCustomPasswordChangeMessageTextRequest) (*mgmt_pb.SetCustomPasswordChangeMessageTextResponse, error) { + result, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, SetPasswordChangeCustomTextToDomain(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.SetCustomPasswordChangeMessageTextResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) ResetCustomPasswordChangeMessageTextToDefault(ctx context.Context, req *mgmt_pb.ResetCustomPasswordChangeMessageTextToDefaultRequest) (*mgmt_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse, error) { + result, err := s.command.RemoveOrgMessageTexts(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordChangeMessageType, language.Make(req.Language)) + if err != nil { + return nil, err + } + return &mgmt_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + func (s *Server) GetCustomPasswordlessRegistrationMessageText(ctx context.Context, req *mgmt_pb.GetCustomPasswordlessRegistrationMessageTextRequest) (*mgmt_pb.GetCustomPasswordlessRegistrationMessageTextResponse, error) { msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordlessRegistrationMessageType, req.Language, false) if err != nil { diff --git a/internal/api/grpc/management/custom_text_converter.go b/internal/api/grpc/management/custom_text_converter.go index 4ff3b389bc..b7569c087d 100644 --- a/internal/api/grpc/management/custom_text_converter.go +++ b/internal/api/grpc/management/custom_text_converter.go @@ -83,6 +83,21 @@ func SetDomainClaimedCustomTextToDomain(msg *mgmt_pb.SetCustomDomainClaimedMessa } } +func SetPasswordChangeCustomTextToDomain(msg *mgmt_pb.SetCustomPasswordChangeMessageTextRequest) *domain.CustomMessageText { + langTag := language.Make(msg.Language) + return &domain.CustomMessageText{ + MessageTextType: domain.PasswordChangeMessageType, + Language: langTag, + Title: msg.Title, + PreHeader: msg.PreHeader, + Subject: msg.Subject, + Greeting: msg.Greeting, + Text: msg.Text, + ButtonText: msg.ButtonText, + FooterText: msg.FooterText, + } +} + func SetPasswordlessRegistrationCustomTextToDomain(msg *mgmt_pb.SetCustomPasswordlessRegistrationMessageTextRequest) *domain.CustomMessageText { langTag := language.Make(msg.Language) return &domain.CustomMessageText{ diff --git a/internal/api/grpc/management/policy_notification.go b/internal/api/grpc/management/policy_notification.go new file mode 100644 index 0000000000..8a4dc76168 --- /dev/null +++ b/internal/api/grpc/management/policy_notification.go @@ -0,0 +1,64 @@ +package management + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/object" + policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy" + mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" +) + +func (s *Server) GetNotificationPolicy(ctx context.Context, _ *mgmt_pb.GetNotificationPolicyRequest) (*mgmt_pb.GetNotificationPolicyResponse, error) { + policy, err := s.query.NotificationPolicyByOrg(ctx, true, authz.GetCtxData(ctx).OrgID, false) + if err != nil { + return nil, err + } + return &mgmt_pb.GetNotificationPolicyResponse{Policy: policy_grpc.ModelNotificationPolicyToPb(policy)}, nil +} + +func (s *Server) GetDefaultNotificationPolicy(ctx context.Context, _ *mgmt_pb.GetDefaultNotificationPolicyRequest) (*mgmt_pb.GetDefaultNotificationPolicyResponse, error) { + policy, err := s.query.DefaultNotificationPolicy(ctx, true) + if err != nil { + return nil, err + } + return &mgmt_pb.GetDefaultNotificationPolicyResponse{Policy: policy_grpc.ModelNotificationPolicyToPb(policy)}, nil +} + +func (s *Server) AddCustomNotificationPolicy(ctx context.Context, req *mgmt_pb.AddCustomNotificationPolicyRequest) (*mgmt_pb.AddCustomNotificationPolicyResponse, error) { + result, err := s.command.AddNotificationPolicy(ctx, authz.GetCtxData(ctx).OrgID, req.GetPasswordChange()) + if err != nil { + return nil, err + } + return &mgmt_pb.AddCustomNotificationPolicyResponse{ + Details: object.AddToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) UpdateCustomNotificationPolicy(ctx context.Context, req *mgmt_pb.UpdateCustomNotificationPolicyRequest) (*mgmt_pb.UpdateCustomNotificationPolicyResponse, error) { + result, err := s.command.ChangeNotificationPolicy(ctx, authz.GetCtxData(ctx).OrgID, req.GetPasswordChange()) + if err != nil { + return nil, err + } + return &mgmt_pb.UpdateCustomNotificationPolicyResponse{ + Details: object.ChangeToDetailsPb( + result.Sequence, + result.EventDate, + result.ResourceOwner, + ), + }, nil +} + +func (s *Server) ResetNotificationPolicyToDefault(ctx context.Context, _ *mgmt_pb.ResetNotificationPolicyToDefaultRequest) (*mgmt_pb.ResetNotificationPolicyToDefaultResponse, error) { + objectDetails, err := s.command.RemoveNotificationPolicy(ctx, authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return &mgmt_pb.ResetNotificationPolicyToDefaultResponse{ + Details: object.DomainToChangeDetailsPb(objectDetails), + }, nil +} diff --git a/internal/api/grpc/policy/notification_policy.go b/internal/api/grpc/policy/notification_policy.go new file mode 100644 index 0000000000..50d74e393c --- /dev/null +++ b/internal/api/grpc/policy/notification_policy.go @@ -0,0 +1,20 @@ +package policy + +import ( + "github.com/zitadel/zitadel/internal/api/grpc/object" + "github.com/zitadel/zitadel/internal/query" + policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy" +) + +func ModelNotificationPolicyToPb(policy *query.NotificationPolicy) *policy_pb.NotificationPolicy { + return &policy_pb.NotificationPolicy{ + IsDefault: policy.IsDefault, + PasswordChange: policy.PasswordChange, + Details: object.ToViewDetailsPb( + policy.Sequence, + policy.CreationDate, + policy.ChangeDate, + policy.ResourceOwner, + ), + } +} diff --git a/internal/api/ui/console/console.go b/internal/api/ui/console/console.go index 914b81f7d3..461dfffe77 100644 --- a/internal/api/ui/console/console.go +++ b/internal/api/ui/console/console.go @@ -52,6 +52,10 @@ var ( } ) +func LoginHintLink(origin, username string) string { + return origin + HandlerPrefix + "?login_hint=" + username +} + func (i *spaHandler) Open(name string) (http.File, error) { ret, err := i.fileSystem.Open(name) if !os.IsNotExist(err) || path.Ext(name) != "" { diff --git a/internal/command/instance.go b/internal/command/instance.go index bb6077f05e..0ccee43935 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -82,6 +82,9 @@ type InstanceSetup struct { SecondFactorCheckLifetime time.Duration MultiFactorCheckLifetime time.Duration } + NotificationPolicy struct { + PasswordChange bool + } PrivacyPolicy struct { TOSLink string PrivacyLink string @@ -236,6 +239,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str prepareAddMultiFactorToDefaultLoginPolicy(instanceAgg, domain.MultiFactorTypeU2FWithPIN), prepareAddDefaultPrivacyPolicy(instanceAgg, setup.PrivacyPolicy.TOSLink, setup.PrivacyPolicy.PrivacyLink, setup.PrivacyPolicy.HelpLink), + prepareAddDefaultNotificationPolicy(instanceAgg, setup.NotificationPolicy.PasswordChange), prepareAddDefaultLockoutPolicy(instanceAgg, setup.LockoutPolicy.MaxAttempts, setup.LockoutPolicy.ShouldShowLockoutFailure), prepareAddDefaultLabelPolicy( diff --git a/internal/command/instance_policy_notification.go b/internal/command/instance_policy_notification.go new file mode 100644 index 0000000000..c3a6574357 --- /dev/null +++ b/internal/command/instance_policy_notification.go @@ -0,0 +1,92 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" +) + +func (c *Commands) AddDefaultNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) { + instanceAgg := instance.NewAggregate(resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddDefaultNotificationPolicy(instanceAgg, passwordChange)) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + +func (c *Commands) ChangeDefaultNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) { + instanceAgg := instance.NewAggregate(resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareChangeDefaultNotificationPolicy(instanceAgg, passwordChange)) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + +func prepareAddDefaultNotificationPolicy( + a *instance.Aggregate, + passwordChange bool, +) preparation.Validation { + return func() (preparation.CreateCommands, error) { + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel := NewInstanceNotificationPolicyWriteModel(ctx) + 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 writeModel.State == domain.PolicyStateActive { + return nil, caos_errs.ThrowAlreadyExists(nil, "INSTANCE-xpo1bj", "Errors.Instance.NotificationPolicy.AlreadyExists") + } + return []eventstore.Command{ + instance.NewNotificationPolicyAddedEvent(ctx, &a.Aggregate, passwordChange), + }, nil + }, nil + } +} + +func prepareChangeDefaultNotificationPolicy( + a *instance.Aggregate, + passwordChange bool, +) preparation.Validation { + return func() (preparation.CreateCommands, error) { + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel := NewInstanceNotificationPolicyWriteModel(ctx) + 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 writeModel.State == domain.PolicyStateUnspecified || writeModel.State == domain.PolicyStateRemoved { + return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-x891na", "Errors.IAM.NotificationPolicy.NotFound") + } + change, hasChanged := writeModel.NewChangedEvent(ctx, &a.Aggregate, passwordChange) + if !hasChanged { + return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-29x02n", "Errors.IAM.NotificationPolicy.NotChanged") + } + return []eventstore.Command{ + change, + }, nil + }, nil + } +} diff --git a/internal/command/instance_policy_notification_model.go b/internal/command/instance_policy_notification_model.go new file mode 100644 index 0000000000..d5eb6d1add --- /dev/null +++ b/internal/command/instance_policy_notification_model.go @@ -0,0 +1,72 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/policy" +) + +type InstanceNotificationPolicyWriteModel struct { + NotificationPolicyWriteModel +} + +func NewInstanceNotificationPolicyWriteModel(ctx context.Context) *InstanceNotificationPolicyWriteModel { + return &InstanceNotificationPolicyWriteModel{ + NotificationPolicyWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: authz.GetInstance(ctx).InstanceID(), + ResourceOwner: authz.GetInstance(ctx).InstanceID(), + }, + }, + } +} + +func (wm *InstanceNotificationPolicyWriteModel) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch e := event.(type) { + case *instance.NotificationPolicyAddedEvent: + wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyAddedEvent) + case *instance.NotificationPolicyChangedEvent: + wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyChangedEvent) + } + } +} + +func (wm *InstanceNotificationPolicyWriteModel) Reduce() error { + return wm.NotificationPolicyWriteModel.Reduce() +} + +func (wm *InstanceNotificationPolicyWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(instance.AggregateType). + AggregateIDs(wm.NotificationPolicyWriteModel.AggregateID). + EventTypes( + instance.NotificationPolicyAddedEventType, + instance.NotificationPolicyChangedEventType). + Builder() +} + +func (wm *InstanceNotificationPolicyWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + passwordChange bool, +) (*instance.NotificationPolicyChangedEvent, bool) { + + changes := make([]policy.NotificationPolicyChanges, 0) + if wm.PasswordChange != passwordChange { + changes = append(changes, policy.ChangePasswordChange(passwordChange)) + } + if len(changes) == 0 { + return nil, false + } + changedEvent, err := instance.NewNotificationPolicyChangedEvent(ctx, aggregate, changes) + if err != nil { + return nil, false + } + return changedEvent, true +} diff --git a/internal/command/instance_policy_notification_test.go b/internal/command/instance_policy_notification_test.go new file mode 100644 index 0000000000..b4639ae5e6 --- /dev/null +++ b/internal/command/instance_policy_notification_test.go @@ -0,0 +1,260 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/policy" +) + +func TestCommandSide_AddDefaultNotificationPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + resourceOwner string + passwordChange bool + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "notification policy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewNotificationPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "INSTANCE", + passwordChange: true, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + instance.NewNotificationPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + true, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "INSTANCE", + passwordChange: true, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "add empty policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + instance.NewNotificationPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + true, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "INSTANCE", + passwordChange: true, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddDefaultNotificationPolicy(tt.args.ctx, tt.args.resourceOwner, tt.args.passwordChange) + 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) + } + }) + } +} + +func TestCommandSide_ChangeDefaultNotificationPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + resourceOwner string + passwordChange bool + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "privacy policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "INSTANCE", + passwordChange: true, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewNotificationPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "INSTANCE", + passwordChange: true, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewNotificationPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultNotificationPolicyChangedEvent(context.Background(), + true, + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "INSTANCE", + passwordChange: true, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeDefaultNotificationPolicy(tt.args.ctx, tt.args.resourceOwner, tt.args.passwordChange) + 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) + } + }) + } +} + +func newDefaultNotificationPolicyChangedEvent(ctx context.Context, passwordChange bool) *instance.NotificationPolicyChangedEvent { + event, _ := instance.NewNotificationPolicyChangedEvent(ctx, + &instance.NewAggregate("INSTANCE").Aggregate, + []policy.NotificationPolicyChanges{ + policy.ChangePasswordChange(passwordChange), + }, + ) + return event +} diff --git a/internal/command/org_converter.go b/internal/command/org_converter.go index 74672e48f3..7533f6963e 100644 --- a/internal/command/org_converter.go +++ b/internal/command/org_converter.go @@ -49,5 +49,6 @@ func orgWriteModelToPrivacyPolicy(wm *OrgPrivacyPolicyWriteModel) *domain.Privac ObjectRoot: writeModelToObjectRoot(wm.PrivacyPolicyWriteModel.WriteModel), TOSLink: wm.TOSLink, PrivacyLink: wm.PrivacyLink, + HelpLink: wm.HelpLink, } } diff --git a/internal/command/org_policy_notification.go b/internal/command/org_policy_notification.go new file mode 100644 index 0000000000..34613d486e --- /dev/null +++ b/internal/command/org_policy_notification.go @@ -0,0 +1,139 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/org" +) + +func (c *Commands) AddNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-x801sk2i", "Errors.ResourceOwnerMissing") + } + orgAgg := org.NewAggregate(resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddNotificationPolicy(orgAgg, passwordChange)) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + +func prepareAddNotificationPolicy( + a *org.Aggregate, + passwordChange bool, +) preparation.Validation { + return func() (preparation.CreateCommands, error) { + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel := NewOrgNotificationPolicyWriteModel(a.Aggregate.ID) + 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 writeModel.State == domain.PolicyStateActive { + return nil, caos_errs.ThrowAlreadyExists(nil, "Org-xa08n2", "Errors.Org.NotificationPolicy.AlreadyExists") + } + return []eventstore.Command{ + org.NewNotificationPolicyAddedEvent(ctx, &a.Aggregate, passwordChange), + }, nil + }, nil + } +} + +func (c *Commands) ChangeNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-x091n1g", "Errors.ResourceOwnerMissing") + } + orgAgg := org.NewAggregate(resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareChangeNotificationPolicy(orgAgg, passwordChange)) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + +func prepareChangeNotificationPolicy( + a *org.Aggregate, + passwordChange bool, +) preparation.Validation { + return func() (preparation.CreateCommands, error) { + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel := NewOrgNotificationPolicyWriteModel(a.Aggregate.ID) + 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 writeModel.State == domain.PolicyStateUnspecified || writeModel.State == domain.PolicyStateRemoved { + return nil, caos_errs.ThrowNotFound(nil, "ORG-x029n3", "Errors.Org.NotificationPolicy.NotFound") + } + change, hasChanged := writeModel.NewChangedEvent(ctx, &a.Aggregate, passwordChange) + if !hasChanged { + return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-ioqnxz", "Errors.Org.NotificationPolicy.NotChanged") + } + return []eventstore.Command{ + change, + }, nil + }, nil + } +} + +func (c *Commands) RemoveNotificationPolicy(ctx context.Context, resourceOwner string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-x89ns2", "Errors.ResourceOwnerMissing") + } + orgAgg := org.NewAggregate(resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveNotificationPolicy(orgAgg)) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + +func prepareRemoveNotificationPolicy( + a *org.Aggregate, +) preparation.Validation { + return func() (preparation.CreateCommands, error) { + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel := NewOrgNotificationPolicyWriteModel(a.Aggregate.ID) + 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 writeModel.State == domain.PolicyStateUnspecified || writeModel.State == domain.PolicyStateRemoved { + return nil, caos_errs.ThrowNotFound(nil, "ORG-x029n1s", "Errors.Org.NotificationPolicy.NotFound") + } + return []eventstore.Command{ + org.NewNotificationPolicyRemovedEvent(ctx, &a.Aggregate), + }, nil + }, nil + } +} diff --git a/internal/command/org_policy_notification_model.go b/internal/command/org_policy_notification_model.go new file mode 100644 index 0000000000..653e4e1e4d --- /dev/null +++ b/internal/command/org_policy_notification_model.go @@ -0,0 +1,73 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/policy" +) + +type OrgNotificationPolicyWriteModel struct { + NotificationPolicyWriteModel +} + +func NewOrgNotificationPolicyWriteModel(orgID string) *OrgNotificationPolicyWriteModel { + return &OrgNotificationPolicyWriteModel{ + NotificationPolicyWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: orgID, + ResourceOwner: orgID, + }, + }, + } +} + +func (wm *OrgNotificationPolicyWriteModel) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch e := event.(type) { + case *org.NotificationPolicyAddedEvent: + wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyAddedEvent) + case *org.NotificationPolicyChangedEvent: + wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyChangedEvent) + case *org.NotificationPolicyRemovedEvent: + wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyRemovedEvent) + } + } +} + +func (wm *OrgNotificationPolicyWriteModel) Reduce() error { + return wm.NotificationPolicyWriteModel.Reduce() +} + +func (wm *OrgNotificationPolicyWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateIDs(wm.NotificationPolicyWriteModel.AggregateID). + AggregateTypes(org.AggregateType). + EventTypes(org.NotificationPolicyAddedEventType, + org.NotificationPolicyChangedEventType, + org.NotificationPolicyRemovedEventType). + Builder() +} + +func (wm *OrgNotificationPolicyWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + passwordChange bool, +) (*org.NotificationPolicyChangedEvent, bool) { + + changes := make([]policy.NotificationPolicyChanges, 0) + if wm.PasswordChange != passwordChange { + changes = append(changes, policy.ChangePasswordChange(passwordChange)) + } + if len(changes) == 0 { + return nil, false + } + changedEvent, err := org.NewNotificationPolicyChangedEvent(ctx, aggregate, changes) + if err != nil { + return nil, false + } + return changedEvent, true +} diff --git a/internal/command/org_policy_notification_test.go b/internal/command/org_policy_notification_test.go new file mode 100644 index 0000000000..7b568ebfdf --- /dev/null +++ b/internal/command/org_policy_notification_test.go @@ -0,0 +1,391 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/policy" +) + +func TestCommandSide_AddNotificationPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + passwordChange bool + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "", + passwordChange: true, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewNotificationPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + passwordChange: true, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewNotificationPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + passwordChange: true, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "add policy empty, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewNotificationPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + false, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + passwordChange: false, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddNotificationPolicy(tt.args.ctx, tt.args.orgID, tt.args.passwordChange) + 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) + } + }) + } +} + +func TestCommandSide_ChangeNotificationPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + passwordChange bool + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + passwordChange: true, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + passwordChange: true, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewNotificationPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + passwordChange: true, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewNotificationPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newNotificationPolicyChangedEvent(context.Background(), "org1", false), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + passwordChange: false, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeNotificationPolicy(tt.args.ctx, tt.args.orgID, tt.args.passwordChange) + 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) + } + }) + } +} + +func TestCommandSide_RemoveNotificationPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewNotificationPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewNotificationPolicyRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveNotificationPolicy(tt.args.ctx, tt.args.orgID) + 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) + } + }) + } +} + +func newNotificationPolicyChangedEvent(ctx context.Context, orgID string, passwordChange bool) *org.NotificationPolicyChangedEvent { + event, _ := org.NewNotificationPolicyChangedEvent(ctx, + &org.NewAggregate(orgID).Aggregate, + []policy.NotificationPolicyChanges{ + policy.ChangePasswordChange(passwordChange), + }, + ) + return event +} diff --git a/internal/command/policy_notification_model.go b/internal/command/policy_notification_model.go new file mode 100644 index 0000000000..06d10504e6 --- /dev/null +++ b/internal/command/policy_notification_model.go @@ -0,0 +1,31 @@ +package command + +import ( + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/policy" +) + +type NotificationPolicyWriteModel struct { + eventstore.WriteModel + + PasswordChange bool + State domain.PolicyState +} + +func (wm *NotificationPolicyWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *policy.NotificationPolicyAddedEvent: + wm.PasswordChange = e.PasswordChange + wm.State = domain.PolicyStateActive + case *policy.NotificationPolicyChangedEvent: + if e.PasswordChange != nil { + wm.PasswordChange = *e.PasswordChange + } + case *policy.NotificationPolicyRemovedEvent: + wm.State = domain.PolicyStateRemoved + } + } + return wm.WriteModel.Reduce() +} diff --git a/internal/command/user_human_password.go b/internal/command/user_human_password.go index 9dbf49924c..706fe2fcc9 100644 --- a/internal/command/user_human_password.go +++ b/internal/command/user_human_password.go @@ -196,6 +196,23 @@ func (c *Commands) PasswordCodeSent(ctx context.Context, orgID, userID string) ( return err } +func (c *Commands) PasswordChangeSent(ctx context.Context, orgID, userID string) (err error) { + if userID == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-pqlm2n", "Errors.User.UserIDMissing") + } + + existingPassword, err := c.passwordWriteModel(ctx, userID, orgID) + if err != nil { + return err + } + if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted { + return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-x902b2v", "Errors.User.NotFound") + } + userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel) + _, err = c.eventstore.Push(ctx, user.NewHumanPasswordChangeSentEvent(ctx, userAgg)) + return err +} + func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, password string, authRequest *domain.AuthRequest, lockoutPolicy *domain.LockoutPolicy) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/domain/custom_message_text.go b/internal/domain/custom_message_text.go index ff0cf7fde7..5109b8d581 100644 --- a/internal/domain/custom_message_text.go +++ b/internal/domain/custom_message_text.go @@ -13,6 +13,7 @@ const ( VerifyPhoneMessageType = "VerifyPhone" DomainClaimedMessageType = "DomainClaimed" PasswordlessRegistrationMessageType = "PasswordlessRegistration" + PasswordChangeMessageType = "PasswordChange" MessageTitle = "Title" MessagePreHeader = "PreHeader" MessageSubject = "Subject" @@ -29,6 +30,7 @@ type MessageTexts struct { VerifyPhone CustomMessageText DomainClaimed CustomMessageText PasswordlessRegistration CustomMessageText + PasswordChange CustomMessageText } type CustomMessageText struct { @@ -65,6 +67,8 @@ func (m *MessageTexts) GetMessageTextByType(msgType string) *CustomMessageText { return &m.DomainClaimed case PasswordlessRegistrationMessageType: return &m.PasswordlessRegistration + case PasswordChangeMessageType: + return &m.PasswordChange } return nil } @@ -75,5 +79,6 @@ func IsMessageTextType(textType string) bool { textType == VerifyEmailMessageType || textType == VerifyPhoneMessageType || textType == DomainClaimedMessageType || - textType == PasswordlessRegistrationMessageType + textType == PasswordlessRegistrationMessageType || + textType == PasswordChangeMessageType } diff --git a/internal/notification/projection.go b/internal/notification/projection.go index 5080e5aa9f..4815a8ef77 100644 --- a/internal/notification/projection.go +++ b/internal/notification/projection.go @@ -137,6 +137,10 @@ func (p *notificationsProjection) reducers() []handler.AggregateReducer { Event: user.HumanPhoneCodeAddedType, Reduce: p.reducePhoneCodeAdded, }, + { + Event: user.HumanPasswordChangedType, + Reduce: p.reducePasswordChanged, + }, }, }, } @@ -463,6 +467,74 @@ func (p *notificationsProjection) reducePasswordlessCodeRequested(event eventsto return crdb.NewNoOpStatement(e), nil } +func (p *notificationsProjection) reducePasswordChanged(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.HumanPasswordChangedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Yko2z8", "reduce.wrong.event.type %s", user.HumanPasswordChangedType) + } + ctx := setNotificationContext(event.Aggregate()) + alreadyHandled, err := p.checkIfAlreadyHandled(ctx, event, nil, user.HumanPasswordChangeSentType) + if err != nil { + return nil, err + } + if alreadyHandled { + return crdb.NewNoOpStatement(e), nil + } + + notificationPolicy, err := p.queries.NotificationPolicyByOrg(ctx, true, e.Aggregate().ResourceOwner, false) + if errors.IsNotFound(err) { + return crdb.NewNoOpStatement(e), nil + } + if err != nil { + return nil, err + } + + if notificationPolicy.PasswordChange { + colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) + if err != nil { + return nil, err + } + + template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false) + if err != nil { + return nil, err + } + + notifyUser, err := p.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID, false) + if err != nil { + return nil, err + } + translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordChangeMessageType) + if err != nil { + return nil, err + } + + ctx, origin, err := p.origin(ctx) + if err != nil { + return nil, err + } + err = types.SendEmail( + ctx, + string(template.Template), + translator, + notifyUser, + p.getSMTPConfig, + p.getFileSystemProvider, + p.getLogProvider, + colors, + p.assetsPrefix(ctx), + ).SendPasswordChange(notifyUser, origin) + if err != nil { + return nil, err + } + err = p.commands.PasswordChangeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + if err != nil { + return nil, err + } + } + return crdb.NewNoOpStatement(e), nil +} + func (p *notificationsProjection) reducePhoneCodeAdded(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*user.HumanPhoneCodeAddedEvent) if !ok { diff --git a/internal/notification/static/i18n/de.yaml b/internal/notification/static/i18n/de.yaml index 0d432259ca..332528ffe6 100644 --- a/internal/notification/static/i18n/de.yaml +++ b/internal/notification/static/i18n/de.yaml @@ -40,3 +40,10 @@ PasswordlessRegistration: Greeting: Hallo {{.FirstName}} {{.LastName}}, Text: Wir haben eine Anfrage für das Hinzufügen eines Token für den passwortlosen Login erhalten. Du kannst den untenstehenden Button verwenden, um dein Token oder Gerät hinzuzufügen. ButtonText: Passwortlosen Login hinzufügen +PasswordChange: + Title: ZITADEL - Passwort von Benutzer wurde geändert + PreHeader: Passwort Änderung + Subject: Passwort von Benutzer wurde geändert + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Das Password vom Benutzer wurde geändert, wenn diese Änderung von jemand anderem gemacht wurde, empfehlen wir die sofortige Zurücksetzung ihres Passworts. + ButtonText: Login \ No newline at end of file diff --git a/internal/notification/static/i18n/en.yaml b/internal/notification/static/i18n/en.yaml index 88b7ae8d62..0608fdbd8b 100644 --- a/internal/notification/static/i18n/en.yaml +++ b/internal/notification/static/i18n/en.yaml @@ -40,3 +40,10 @@ PasswordlessRegistration: Greeting: Hello {{.FirstName}} {{.LastName}}, Text: We received a request to add a token for passwordless login. Please use the button below to add your token or device for passwordless login. ButtonText: Add Passwordless Login +PasswordChange: + Title: ZITADEL - Password of user has changed + PreHeader: Change password + Subject: Password of user has changed + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: The password of your user has changed, if this change was not done by you, please be advised to immediately reset your password. + ButtonText: Login \ No newline at end of file diff --git a/internal/notification/static/i18n/fr.yaml b/internal/notification/static/i18n/fr.yaml index 243ef64932..4bf7d2eb3c 100644 --- a/internal/notification/static/i18n/fr.yaml +++ b/internal/notification/static/i18n/fr.yaml @@ -40,3 +40,10 @@ PasswordlessRegistration: Greeting: Bonjour {{.FirstName}} {{.LastName}}, Text: Nous avons reçu une demande d'ajout d'un jeton pour la connexion sans mot de passe. Veuillez utiliser le bouton ci-dessous pour ajouter votre jeton ou dispositif pour la connexion sans mot de passe. ButtonText: Ajouter une connexion sans mot de passe +PasswordChange: + Title: ZITADEL - Le mot de passe de l'utilisateur a changé + PreHeader: Modifier le mot de passe + Subject: Le mot de passe de l'utilisateur a changé + Greeting: Bonjour {{.FirstName}} {{.LastName}}, + Text: Le mot de passe de votre utilisateur a changé, si ce changement n'a pas été fait par vous, nous vous conseillons de réinitialiser immédiatement votre mot de passe. + ButtonText: Login diff --git a/internal/notification/static/i18n/it.yaml b/internal/notification/static/i18n/it.yaml index 8f210e1b84..97b541d8b2 100644 --- a/internal/notification/static/i18n/it.yaml +++ b/internal/notification/static/i18n/it.yaml @@ -40,3 +40,10 @@ PasswordlessRegistration: Greeting: 'Ciao {{.FirstName}} {{.LastName}},' Text: Abbiamo ricevuto una richiesta per aggiungere l'autenticazione passwordless. Usa il pulsante qui sotto per aggiungere il tuo token o dispositivo per il login senza password. ButtonText: Attiva passwordless +PasswordChange: + Title: ZITADEL - La password dell'utente è stata modificata + PreHeader: Modifica della password + Subject: La password dell'utente è stata modificata + Greeting: Ciao {{.FirstName}} {{.LastName}}, + Text: La password del vostro utente è cambiata; se questa modifica non è stata fatta da voi, vi consigliamo di reimpostare immediatamente la vostra password. + ButtonText: Login \ No newline at end of file diff --git a/internal/notification/static/i18n/zh.yaml b/internal/notification/static/i18n/zh.yaml index 1b2bff2a4e..e3f9907596 100644 --- a/internal/notification/static/i18n/zh.yaml +++ b/internal/notification/static/i18n/zh.yaml @@ -40,3 +40,10 @@ PasswordlessRegistration: Greeting: 你好 {{.FirstName}} {{.LastName}}, Text: 我们收到了为无密码登录添加令牌的请求。请使用下面的按钮添加您的令牌或设备以进行无密码登录。 ButtonText: 添加无密码登录 +PasswordChange: + Title: ZITADEL - 用户的密码已经改变 + PreHeader: 更改密码 + Subject: 用户的密码已经改变 + Greeting: 你好 {{.FirstName}} {{.LastName}}, + Text: 您的用户的密码已经改变,如果这个改变不是由您做的,请注意立即重新设置您的密码。 + ButtonText: 登录 diff --git a/internal/notification/types/password_change.go b/internal/notification/types/password_change.go new file mode 100644 index 0000000000..7b963c2d09 --- /dev/null +++ b/internal/notification/types/password_change.go @@ -0,0 +1,13 @@ +package types + +import ( + "github.com/zitadel/zitadel/internal/api/ui/console" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" +) + +func (notify Notify) SendPasswordChange(user *query.NotifyUser, origin string) error { + url := console.LoginHintLink(origin, user.PreferredLoginName) + args := make(map[string]interface{}) + return notify(url, args, domain.PasswordChangeMessageType, true) +} diff --git a/internal/query/message_text.go b/internal/query/message_text.go index 9fa7ffabdd..0312e9e3d9 100644 --- a/internal/query/message_text.go +++ b/internal/query/message_text.go @@ -29,6 +29,7 @@ type MessageTexts struct { VerifyPhone MessageText DomainClaimed MessageText PasswordlessRegistration MessageText + PasswordChange MessageText } type MessageText struct { @@ -330,6 +331,8 @@ func (m *MessageTexts) GetMessageTextByType(msgType string) *MessageText { return &m.DomainClaimed case domain.PasswordlessRegistrationMessageType: return &m.PasswordlessRegistration + case domain.PasswordChangeMessageType: + return &m.PasswordChange } return nil } diff --git a/internal/query/notification_policy.go b/internal/query/notification_policy.go new file mode 100644 index 0000000000..1ae61e8fa8 --- /dev/null +++ b/internal/query/notification_policy.go @@ -0,0 +1,166 @@ +package query + +import ( + "context" + "database/sql" + errs "errors" + "time" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +type NotificationPolicy struct { + ID string + Sequence uint64 + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + State domain.PolicyState + + PasswordChange bool + + IsDefault bool +} + +var ( + notificationPolicyTable = table{ + name: projection.NotificationPolicyProjectionTable, + instanceIDCol: projection.NotificationPolicyColumnInstanceID, + } + NotificationPolicyColID = Column{ + name: projection.NotificationPolicyColumnID, + table: notificationPolicyTable, + } + NotificationPolicyColSequence = Column{ + name: projection.NotificationPolicyColumnSequence, + table: notificationPolicyTable, + } + NotificationPolicyColCreationDate = Column{ + name: projection.NotificationPolicyColumnCreationDate, + table: notificationPolicyTable, + } + NotificationPolicyColChangeDate = Column{ + name: projection.NotificationPolicyColumnChangeDate, + table: notificationPolicyTable, + } + NotificationPolicyColResourceOwner = Column{ + name: projection.NotificationPolicyColumnResourceOwner, + table: notificationPolicyTable, + } + NotificationPolicyColInstanceID = Column{ + name: projection.NotificationPolicyColumnInstanceID, + table: notificationPolicyTable, + } + NotificationPolicyColPasswordChange = Column{ + name: projection.NotificationPolicyColumnPasswordChange, + table: notificationPolicyTable, + } + NotificationPolicyColIsDefault = Column{ + name: projection.NotificationPolicyColumnIsDefault, + table: notificationPolicyTable, + } + NotificationPolicyColState = Column{ + name: projection.NotificationPolicyColumnStateCol, + table: notificationPolicyTable, + } + NotificationPolicyColOwnerRemoved = Column{ + name: projection.NotificationPolicyColumnOwnerRemoved, + table: notificationPolicyTable, + } +) + +func (q *Queries) NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, orgID string, withOwnerRemoved bool) (_ *NotificationPolicy, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if shouldTriggerBulk { + if err := projection.NotificationPolicyProjection.Trigger(ctx); err != nil { + return nil, err + } + } + eq := sq.Eq{NotificationPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} + if !withOwnerRemoved { + eq[NotificationPolicyColOwnerRemoved.identifier()] = false + } + stmt, scan := prepareNotificationPolicyQuery() + query, args, err := stmt.Where( + sq.And{ + eq, + sq.Or{ + sq.Eq{NotificationPolicyColID.identifier(): orgID}, + sq.Eq{NotificationPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID()}, + }, + }). + OrderBy(NotificationPolicyColIsDefault.identifier()).Limit(1).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Xuoapqm", "Errors.Query.SQLStatement") + } + + row := q.client.QueryRowContext(ctx, query, args...) + return scan(row) +} + +func (q *Queries) DefaultNotificationPolicy(ctx context.Context, shouldTriggerBulk bool) (_ *NotificationPolicy, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if shouldTriggerBulk { + if err := projection.NotificationPolicyProjection.Trigger(ctx); err != nil { + return nil, err + } + } + + stmt, scan := prepareNotificationPolicyQuery() + query, args, err := stmt.Where(sq.Eq{ + NotificationPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID(), + NotificationPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + }). + OrderBy(NotificationPolicyColIsDefault.identifier()). + Limit(1).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-xlqp209", "Errors.Query.SQLStatement") + } + + row := q.client.QueryRowContext(ctx, query, args...) + return scan(row) +} + +func prepareNotificationPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*NotificationPolicy, error)) { + return sq.Select( + NotificationPolicyColID.identifier(), + NotificationPolicyColSequence.identifier(), + NotificationPolicyColCreationDate.identifier(), + NotificationPolicyColChangeDate.identifier(), + NotificationPolicyColResourceOwner.identifier(), + NotificationPolicyColPasswordChange.identifier(), + NotificationPolicyColIsDefault.identifier(), + NotificationPolicyColState.identifier(), + ). + From(notificationPolicyTable.identifier()).PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*NotificationPolicy, error) { + policy := new(NotificationPolicy) + err := row.Scan( + &policy.ID, + &policy.Sequence, + &policy.CreationDate, + &policy.ChangeDate, + &policy.ResourceOwner, + &policy.PasswordChange, + &policy.IsDefault, + &policy.State, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-x0so2p", "Errors.NotificationPolicy.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-Zixoooq", "Errors.Internal") + } + return policy, nil + } +} diff --git a/internal/query/notification_policy_test.go b/internal/query/notification_policy_test.go new file mode 100644 index 0000000000..b46b597c9a --- /dev/null +++ b/internal/query/notification_policy_test.go @@ -0,0 +1,116 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/zitadel/zitadel/internal/domain" + errs "github.com/zitadel/zitadel/internal/errors" +) + +var notificationPolicyStmt = regexp.QuoteMeta(`SELECT projections.notification_policies.id,` + + ` projections.notification_policies.sequence,` + + ` projections.notification_policies.creation_date,` + + ` projections.notification_policies.change_date,` + + ` projections.notification_policies.resource_owner,` + + ` projections.notification_policies.password_change,` + + ` projections.notification_policies.is_default,` + + ` projections.notification_policies.state` + + ` FROM projections.notification_policies`) + +func Test_NotificationPolicyPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareNotificationPolicyQuery no result", + prepare: prepareNotificationPolicyQuery, + want: want{ + sqlExpectations: mockQueries( + notificationPolicyStmt, + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*NotificationPolicy)(nil), + }, + { + name: "prepareNotificationPolicyQuery found", + prepare: prepareNotificationPolicyQuery, + want: want{ + sqlExpectations: mockQuery( + notificationPolicyStmt, + []string{ + "id", + "sequence", + "creation_date", + "change_date", + "resource_owner", + "password_change", + "is_default", + "state", + }, + []driver.Value{ + "pol-id", + uint64(20211109), + testNow, + testNow, + "ro", + true, + true, + domain.PolicyStateActive, + }, + ), + }, + object: &NotificationPolicy{ + ID: "pol-id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + ResourceOwner: "ro", + State: domain.PolicyStateActive, + PasswordChange: true, + IsDefault: true, + }, + }, + { + name: "prepareNotificationPolicyQuery sql err", + prepare: prepareNotificationPolicyQuery, + want: want{ + sqlExpectations: mockQueryErr( + notificationPolicyStmt, + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} diff --git a/internal/query/projection/message_texts.go b/internal/query/projection/message_texts.go index ab005938d4..5d0e941ea1 100644 --- a/internal/query/projection/message_texts.go +++ b/internal/query/projection/message_texts.go @@ -273,7 +273,8 @@ func isMessageTemplate(template string) bool { template == domain.VerifyEmailMessageType || template == domain.VerifyPhoneMessageType || template == domain.DomainClaimedMessageType || - template == domain.PasswordlessRegistrationMessageType + template == domain.PasswordlessRegistrationMessageType || + template == domain.PasswordChangeMessageType } func isTitle(key string) bool { return key == domain.MessageTitle diff --git a/internal/query/projection/notification_policy.go b/internal/query/projection/notification_policy.go new file mode 100644 index 0000000000..628e322cc5 --- /dev/null +++ b/internal/query/projection/notification_policy.go @@ -0,0 +1,187 @@ +package projection + +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/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/policy" +) + +const ( + NotificationPolicyProjectionTable = "projections.notification_policies" + + NotificationPolicyColumnID = "id" + NotificationPolicyColumnCreationDate = "creation_date" + NotificationPolicyColumnChangeDate = "change_date" + NotificationPolicyColumnResourceOwner = "resource_owner" + NotificationPolicyColumnInstanceID = "instance_id" + NotificationPolicyColumnSequence = "sequence" + NotificationPolicyColumnStateCol = "state" + NotificationPolicyColumnIsDefault = "is_default" + NotificationPolicyColumnPasswordChange = "password_change" + NotificationPolicyColumnOwnerRemoved = "owner_removed" +) + +type notificationPolicyProjection struct { + crdb.StatementHandler +} + +func newNotificationPolicyProjection(ctx context.Context, config crdb.StatementHandlerConfig) *notificationPolicyProjection { + p := new(notificationPolicyProjection) + config.ProjectionName = NotificationPolicyProjectionTable + config.Reducers = p.reducers() + config.InitCheck = crdb.NewTableCheck( + crdb.NewTable([]*crdb.Column{ + crdb.NewColumn(NotificationPolicyColumnID, crdb.ColumnTypeText), + crdb.NewColumn(NotificationPolicyColumnCreationDate, crdb.ColumnTypeTimestamp), + crdb.NewColumn(NotificationPolicyColumnChangeDate, crdb.ColumnTypeTimestamp), + crdb.NewColumn(NotificationPolicyColumnResourceOwner, crdb.ColumnTypeText), + crdb.NewColumn(NotificationPolicyColumnInstanceID, crdb.ColumnTypeText), + crdb.NewColumn(NotificationPolicyColumnSequence, crdb.ColumnTypeInt64), + crdb.NewColumn(NotificationPolicyColumnStateCol, crdb.ColumnTypeEnum), + crdb.NewColumn(NotificationPolicyColumnIsDefault, crdb.ColumnTypeBool), + crdb.NewColumn(NotificationPolicyColumnPasswordChange, crdb.ColumnTypeBool), + crdb.NewColumn(NotificationPolicyColumnOwnerRemoved, crdb.ColumnTypeBool, crdb.Default(false)), + }, + crdb.NewPrimaryKey(NotificationPolicyColumnInstanceID, NotificationPolicyColumnID), + ), + ) + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *notificationPolicyProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: org.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: org.NotificationPolicyAddedEventType, + Reduce: p.reduceAdded, + }, + { + Event: org.NotificationPolicyChangedEventType, + Reduce: p.reduceChanged, + }, + { + Event: org.NotificationPolicyRemovedEventType, + Reduce: p.reduceRemoved, + }, + { + Event: org.OrgRemovedEventType, + Reduce: p.reduceOwnerRemoved, + }, + }, + }, + { + Aggregate: instance.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: instance.InstanceRemovedEventType, + Reduce: reduceInstanceRemovedHelper(NotificationPolicyColumnInstanceID), + }, + { + Event: instance.NotificationPolicyAddedEventType, + Reduce: p.reduceAdded, + }, + { + Event: instance.NotificationPolicyChangedEventType, + Reduce: p.reduceChanged, + }, + }, + }, + } +} + +func (p *notificationPolicyProjection) reduceAdded(event eventstore.Event) (*handler.Statement, error) { + var policyEvent policy.NotificationPolicyAddedEvent + var isDefault bool + switch e := event.(type) { + case *org.NotificationPolicyAddedEvent: + policyEvent = e.NotificationPolicyAddedEvent + isDefault = false + case *instance.NotificationPolicyAddedEvent: + policyEvent = e.NotificationPolicyAddedEvent + isDefault = true + default: + return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-x02s1m", "reduce.wrong.event.type %v", []eventstore.EventType{org.NotificationPolicyAddedEventType, instance.NotificationPolicyAddedEventType}) + } + return crdb.NewCreateStatement( + &policyEvent, + []handler.Column{ + handler.NewCol(NotificationPolicyColumnCreationDate, policyEvent.CreationDate()), + handler.NewCol(NotificationPolicyColumnChangeDate, policyEvent.CreationDate()), + handler.NewCol(NotificationPolicyColumnSequence, policyEvent.Sequence()), + handler.NewCol(NotificationPolicyColumnID, policyEvent.Aggregate().ID), + handler.NewCol(NotificationPolicyColumnStateCol, domain.PolicyStateActive), + handler.NewCol(NotificationPolicyColumnPasswordChange, policyEvent.PasswordChange), + handler.NewCol(NotificationPolicyColumnIsDefault, isDefault), + handler.NewCol(NotificationPolicyColumnResourceOwner, policyEvent.Aggregate().ResourceOwner), + handler.NewCol(NotificationPolicyColumnInstanceID, policyEvent.Aggregate().InstanceID), + }), nil +} + +func (p *notificationPolicyProjection) reduceChanged(event eventstore.Event) (*handler.Statement, error) { + var policyEvent policy.NotificationPolicyChangedEvent + switch e := event.(type) { + case *org.NotificationPolicyChangedEvent: + policyEvent = e.NotificationPolicyChangedEvent + case *instance.NotificationPolicyChangedEvent: + policyEvent = e.NotificationPolicyChangedEvent + default: + return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-psom2h19", "reduce.wrong.event.type %v", []eventstore.EventType{org.NotificationPolicyChangedEventType, instance.NotificationPolicyChangedEventType}) + } + cols := []handler.Column{ + handler.NewCol(NotificationPolicyColumnChangeDate, policyEvent.CreationDate()), + handler.NewCol(NotificationPolicyColumnSequence, policyEvent.Sequence()), + } + if policyEvent.PasswordChange != nil { + cols = append(cols, handler.NewCol(NotificationPolicyColumnPasswordChange, *policyEvent.PasswordChange)) + } + return crdb.NewUpdateStatement( + &policyEvent, + cols, + []handler.Condition{ + handler.NewCond(NotificationPolicyColumnID, policyEvent.Aggregate().ID), + handler.NewCond(NotificationPolicyColumnInstanceID, policyEvent.Aggregate().InstanceID), + }), nil +} + +func (p *notificationPolicyProjection) reduceRemoved(event eventstore.Event) (*handler.Statement, error) { + policyEvent, ok := event.(*org.NotificationPolicyRemovedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-Po2iso2", "reduce.wrong.event.type %s", org.NotificationPolicyRemovedEventType) + } + return crdb.NewDeleteStatement( + policyEvent, + []handler.Condition{ + handler.NewCond(NotificationPolicyColumnID, policyEvent.Aggregate().ID), + handler.NewCond(NotificationPolicyColumnInstanceID, policyEvent.Aggregate().InstanceID), + }), nil +} + +func (p *notificationPolicyProjection) reduceOwnerRemoved(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*org.OrgRemovedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-poxi9a", "reduce.wrong.event.type %s", org.OrgRemovedEventType) + } + + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(DomainPolicyChangeDateCol, e.CreationDate()), + handler.NewCol(DomainPolicySequenceCol, e.Sequence()), + handler.NewCol(DomainPolicyOwnerRemovedCol, true), + }, + []handler.Condition{ + handler.NewCond(DomainPolicyInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCond(DomainPolicyResourceOwnerCol, e.Aggregate().ID), + }, + ), nil +} diff --git a/internal/query/projection/notification_policy_test.go b/internal/query/projection/notification_policy_test.go new file mode 100644 index 0000000000..86a9ed1b65 --- /dev/null +++ b/internal/query/projection/notification_policy_test.go @@ -0,0 +1,258 @@ +package projection + +import ( + "testing" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" +) + +func TestNotificationPolicyProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "org reduceAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.NotificationPolicyAddedEventType), + org.AggregateType, + []byte(`{ + "passwordChange": true +}`), + ), org.NotificationPolicyAddedEventMapper), + }, + reduce: (¬ificationPolicyProjection{}).reduceAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.notification_policies (creation_date, change_date, sequence, id, state, password_change, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + anyArg{}, + anyArg{}, + uint64(15), + "agg-id", + domain.PolicyStateActive, + true, + false, + "ro-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "org reduceChanged", + reduce: (¬ificationPolicyProjection{}).reduceChanged, + args: args{ + event: getEvent(testEvent( + repository.EventType(org.NotificationPolicyChangedEventType), + org.AggregateType, + []byte(`{ + "passwordChange": true + }`), + ), org.NotificationPolicyChangedEventMapper), + }, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.notification_policies SET (change_date, sequence, password_change) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + true, + "agg-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "org reduceRemoved", + reduce: (¬ificationPolicyProjection{}).reduceRemoved, + args: args{ + event: getEvent(testEvent( + repository.EventType(org.NotificationPolicyRemovedEventType), + org.AggregateType, + nil, + ), org.NotificationPolicyRemovedEventMapper), + }, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.notification_policies WHERE (id = $1) AND (instance_id = $2)", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + }, + }, + }, + }, + }, + }, { + name: "instance reduceInstanceRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.InstanceRemovedEventType), + instance.AggregateType, + nil, + ), instance.InstanceRemovedEventMapper), + }, + reduce: reduceInstanceRemovedHelper(NotificationPolicyColumnInstanceID), + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.notification_policies WHERE (instance_id = $1)", + expectedArgs: []interface{}{ + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceAdded", + reduce: (¬ificationPolicyProjection{}).reduceAdded, + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.NotificationPolicyAddedEventType), + instance.AggregateType, + []byte(`{ + "passwordChange": true + }`), + ), instance.NotificationPolicyAddedEventMapper), + }, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.notification_policies (creation_date, change_date, sequence, id, state, password_change, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + anyArg{}, + anyArg{}, + uint64(15), + "agg-id", + domain.PolicyStateActive, + true, + true, + "ro-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceChanged", + reduce: (¬ificationPolicyProjection{}).reduceChanged, + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.NotificationPolicyChangedEventType), + instance.AggregateType, + []byte(`{ + "passwordChange": true + }`), + ), instance.NotificationPolicyChangedEventMapper), + }, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.notification_policies SET (change_date, sequence, password_change) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + true, + "agg-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "org.reduceOwnerRemoved", + reduce: (¬ificationPolicyProjection{}).reduceOwnerRemoved, + args: args{ + event: getEvent(testEvent( + repository.EventType(org.OrgRemovedEventType), + org.AggregateType, + nil, + ), org.OrgRemovedEventMapper), + }, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.notification_policies SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (resource_owner = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + true, + "instance-id", + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + + if ok := errors.IsErrorInvalidArgument(err); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, NotificationPolicyProjectionTable, tt.want) + }) + } +} diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index a5b5fda15c..18462f6204 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -60,6 +60,7 @@ var ( DebugNotificationProviderProjection *debugNotificationProviderProjection KeyProjection *keyProjection SecurityPolicyProjection *securityPolicyProjection + NotificationPolicyProjection *notificationPolicyProjection NotificationsProjection interface{} ) @@ -133,6 +134,7 @@ func Create(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, c DebugNotificationProviderProjection = newDebugNotificationProviderProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["debug_notification_provider"])) KeyProjection = newKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["keys"]), keyEncryptionAlgorithm, certEncryptionAlgorithm) SecurityPolicyProjection = newSecurityPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["security_policies"])) + NotificationPolicyProjection = newNotificationPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["notification_policies"])) newProjectionsList() return nil } @@ -224,5 +226,6 @@ func newProjectionsList() { DebugNotificationProviderProjection, KeyProjection, SecurityPolicyProjection, + NotificationPolicyProjection, } } diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index 7f5176ff2a..4be0a9e02b 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -89,5 +89,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, InstanceDomainRemovedEventType, DomainRemovedEventMapper). RegisterFilterEventMapper(AggregateType, InstanceAddedEventType, InstanceAddedEventMapper). RegisterFilterEventMapper(AggregateType, InstanceChangedEventType, InstanceChangedEventMapper). - RegisterFilterEventMapper(AggregateType, InstanceRemovedEventType, InstanceRemovedEventMapper) + RegisterFilterEventMapper(AggregateType, InstanceRemovedEventType, InstanceRemovedEventMapper). + RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper). + RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper) } diff --git a/internal/repository/instance/policy_notification.go b/internal/repository/instance/policy_notification.go new file mode 100644 index 0000000000..ae8eb7a308 --- /dev/null +++ b/internal/repository/instance/policy_notification.go @@ -0,0 +1,73 @@ +package instance + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/repository/policy" +) + +const ( + NotificationPolicyAddedEventType = instanceEventTypePrefix + policy.NotificationPolicyAddedEventType + NotificationPolicyChangedEventType = instanceEventTypePrefix + policy.NotificationPolicyChangedEventType +) + +type NotificationPolicyAddedEvent struct { + policy.NotificationPolicyAddedEvent +} + +func NewNotificationPolicyAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + passwordChange bool, +) *NotificationPolicyAddedEvent { + return &NotificationPolicyAddedEvent{ + NotificationPolicyAddedEvent: *policy.NewNotificationPolicyAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + NotificationPolicyAddedEventType), + passwordChange), + } +} + +func NotificationPolicyAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := policy.NotificationPolicyAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &NotificationPolicyAddedEvent{NotificationPolicyAddedEvent: *e.(*policy.NotificationPolicyAddedEvent)}, nil +} + +type NotificationPolicyChangedEvent struct { + policy.NotificationPolicyChangedEvent +} + +func NewNotificationPolicyChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + changes []policy.NotificationPolicyChanges, +) (*NotificationPolicyChangedEvent, error) { + changedEvent, err := policy.NewNotificationPolicyChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + NotificationPolicyChangedEventType), + changes, + ) + if err != nil { + return nil, err + } + return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *changedEvent}, nil +} + +func NotificationPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := policy.NotificationPolicyChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *e.(*policy.NotificationPolicyChangedEvent)}, nil +} diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index 6fbd87d1d8..fecf31bd37 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -83,5 +83,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, FlowClearedEventType, FlowClearedEventMapper). RegisterFilterEventMapper(AggregateType, MetadataSetType, MetadataSetEventMapper). RegisterFilterEventMapper(AggregateType, MetadataRemovedType, MetadataRemovedEventMapper). - RegisterFilterEventMapper(AggregateType, MetadataRemovedAllType, MetadataRemovedAllEventMapper) + RegisterFilterEventMapper(AggregateType, MetadataRemovedAllType, MetadataRemovedAllEventMapper). + RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper). + RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper). + RegisterFilterEventMapper(AggregateType, NotificationPolicyRemovedEventType, NotificationPolicyRemovedEventMapper) } diff --git a/internal/repository/org/policy_notification.go b/internal/repository/org/policy_notification.go new file mode 100644 index 0000000000..0c436e9028 --- /dev/null +++ b/internal/repository/org/policy_notification.go @@ -0,0 +1,102 @@ +package org + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/repository/policy" +) + +var ( + NotificationPolicyAddedEventType = orgEventTypePrefix + policy.NotificationPolicyAddedEventType + NotificationPolicyChangedEventType = orgEventTypePrefix + policy.NotificationPolicyChangedEventType + NotificationPolicyRemovedEventType = orgEventTypePrefix + policy.NotificationPolicyRemovedEventType +) + +type NotificationPolicyAddedEvent struct { + policy.NotificationPolicyAddedEvent +} + +func NewNotificationPolicyAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + passwordChange bool, +) *NotificationPolicyAddedEvent { + return &NotificationPolicyAddedEvent{ + NotificationPolicyAddedEvent: *policy.NewNotificationPolicyAddedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + NotificationPolicyAddedEventType), + passwordChange, + ), + } +} + +func NotificationPolicyAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := policy.NotificationPolicyAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &NotificationPolicyAddedEvent{NotificationPolicyAddedEvent: *e.(*policy.NotificationPolicyAddedEvent)}, nil +} + +type NotificationPolicyChangedEvent struct { + policy.NotificationPolicyChangedEvent +} + +func NewNotificationPolicyChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + changes []policy.NotificationPolicyChanges, +) (*NotificationPolicyChangedEvent, error) { + changedEvent, err := policy.NewNotificationPolicyChangedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + NotificationPolicyChangedEventType), + changes, + ) + if err != nil { + return nil, err + } + return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *changedEvent}, nil +} + +func NotificationPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := policy.NotificationPolicyChangedEventMapper(event) + if err != nil { + return nil, err + } + + return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *e.(*policy.NotificationPolicyChangedEvent)}, nil +} + +type NotificationPolicyRemovedEvent struct { + policy.NotificationPolicyRemovedEvent +} + +func NewNotificationPolicyRemovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *NotificationPolicyRemovedEvent { + return &NotificationPolicyRemovedEvent{ + NotificationPolicyRemovedEvent: *policy.NewNotificationPolicyRemovedEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + NotificationPolicyRemovedEventType), + ), + } +} + +func NotificationPolicyRemovedEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := policy.NotificationPolicyRemovedEventMapper(event) + if err != nil { + return nil, err + } + + return &NotificationPolicyRemovedEvent{NotificationPolicyRemovedEvent: *e.(*policy.NotificationPolicyRemovedEvent)}, nil +} diff --git a/internal/repository/policy/policy_notification.go b/internal/repository/policy/policy_notification.go new file mode 100644 index 0000000000..0efb12a12c --- /dev/null +++ b/internal/repository/policy/policy_notification.go @@ -0,0 +1,127 @@ +package policy + +import ( + "encoding/json" + + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" +) + +const ( + NotificationPolicyAddedEventType = "policy.notification.added" + NotificationPolicyChangedEventType = "policy.notification.changed" + NotificationPolicyRemovedEventType = "policy.notification.removed" +) + +type NotificationPolicyAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + PasswordChange bool `json:"passwordChange,omitempty"` +} + +func (e *NotificationPolicyAddedEvent) Data() interface{} { + return e +} + +func (e *NotificationPolicyAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewNotificationPolicyAddedEvent( + base *eventstore.BaseEvent, + passwordChange bool, +) *NotificationPolicyAddedEvent { + return &NotificationPolicyAddedEvent{ + BaseEvent: *base, + PasswordChange: passwordChange, + } +} + +func NotificationPolicyAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &NotificationPolicyAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "POLIC-0sp2nios", "unable to unmarshal policy") + } + + return e, nil +} + +type NotificationPolicyChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + PasswordChange *bool `json:"passwordChange,omitempty"` +} + +func (e *NotificationPolicyChangedEvent) Data() interface{} { + return e +} + +func (e *NotificationPolicyChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewNotificationPolicyChangedEvent( + base *eventstore.BaseEvent, + changes []NotificationPolicyChanges, +) (*NotificationPolicyChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "POLICY-09sp2m", "Errors.NoChangesFound") + } + changeEvent := &NotificationPolicyChangedEvent{ + BaseEvent: *base, + } + for _, change := range changes { + change(changeEvent) + } + return changeEvent, nil +} + +type NotificationPolicyChanges func(*NotificationPolicyChangedEvent) + +func ChangePasswordChange(passwordChange bool) func(*NotificationPolicyChangedEvent) { + return func(e *NotificationPolicyChangedEvent) { + e.PasswordChange = &passwordChange + } +} + +func NotificationPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &NotificationPolicyChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "POLIC-09s2oss", "unable to unmarshal policy") + } + + return e, nil +} + +type NotificationPolicyRemovedEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *NotificationPolicyRemovedEvent) Data() interface{} { + return nil +} + +func (e *NotificationPolicyRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewNotificationPolicyRemovedEvent(base *eventstore.BaseEvent) *NotificationPolicyRemovedEvent { + return &NotificationPolicyRemovedEvent{ + BaseEvent: *base, + } +} + +func NotificationPolicyRemovedEventMapper(event *repository.Event) (eventstore.Event, error) { + return &NotificationPolicyRemovedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + }, nil +} diff --git a/internal/repository/user/eventstore.go b/internal/repository/user/eventstore.go index c0cb3527dd..d925a50654 100644 --- a/internal/repository/user/eventstore.go +++ b/internal/repository/user/eventstore.go @@ -59,6 +59,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, HumanPasswordChangedType, HumanPasswordChangedEventMapper). RegisterFilterEventMapper(AggregateType, HumanPasswordCodeAddedType, HumanPasswordCodeAddedEventMapper). RegisterFilterEventMapper(AggregateType, HumanPasswordCodeSentType, HumanPasswordCodeSentEventMapper). + RegisterFilterEventMapper(AggregateType, HumanPasswordChangeSentType, HumanPasswordChangeSentEventMapper). RegisterFilterEventMapper(AggregateType, HumanPasswordCheckSucceededType, HumanPasswordCheckSucceededEventMapper). RegisterFilterEventMapper(AggregateType, HumanPasswordCheckFailedType, HumanPasswordCheckFailedEventMapper). RegisterFilterEventMapper(AggregateType, UserIDPLinkAddedType, UserIDPLinkAddedEventMapper). diff --git a/internal/repository/user/human_password.go b/internal/repository/user/human_password.go index 6e315ce22f..26343d664d 100644 --- a/internal/repository/user/human_password.go +++ b/internal/repository/user/human_password.go @@ -16,6 +16,7 @@ import ( const ( passwordEventPrefix = humanEventPrefix + "password." HumanPasswordChangedType = passwordEventPrefix + "changed" + HumanPasswordChangeSentType = passwordEventPrefix + "change.sent" HumanPasswordCodeAddedType = passwordEventPrefix + "code.added" HumanPasswordCodeSentType = passwordEventPrefix + "code.sent" HumanPasswordCheckSucceededType = passwordEventPrefix + "check.succeeded" @@ -144,6 +145,34 @@ func HumanPasswordCodeSentEventMapper(event *repository.Event) (eventstore.Event }, nil } +type HumanPasswordChangeSentEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *HumanPasswordChangeSentEvent) Data() interface{} { + return nil +} + +func (e *HumanPasswordChangeSentEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewHumanPasswordChangeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanPasswordChangeSentEvent { + return &HumanPasswordChangeSentEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + HumanPasswordChangeSentType, + ), + } +} + +func HumanPasswordChangeSentEventMapper(event *repository.Event) (eventstore.Event, error) { + return &HumanPasswordChangeSentEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + }, nil +} + type HumanPasswordCheckSucceededEvent struct { eventstore.BaseEvent `json:"-"` *AuthRequestInfo diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 47b68a6ea3..66792b2aae 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -225,6 +225,10 @@ Errors: Empty: Org IAM Policy ist leer NotExisting: Org IAM Policy existiert nicht AlreadyExists: Org IAM Policy existiert bereits + NotificationPolicy: + NotFound: Notification Policy konnte nicht gefunden werden + NotChanged: Notification Policy wurde nicht verändert + AlreadyExists: Notification Policy existiert bereits Project: ProjectIDMissing: Project ID fehlt AlreadyExists: Project existiert bereits auf der Organisation @@ -351,6 +355,10 @@ Errors: AlreadyExists: Default Org IAM Policy existiert bereits Empty: Default Org IAM Policy leer NotChanged: Default Org IAM Policy wurde nicht verändert + NotificationPolicy: + NotFound: Default Notification Policy konnte nicht gefunden werden + NotChanged: Default Notification Policy wurde nicht verändert + AlreadyExists: Default Notification Policy existiert bereits Policy: AlreadyExists: Policy existiert bereits Label: @@ -750,6 +758,10 @@ EventTypes: added: Passwortaussperrrichtlinie hinzugefügt changed: Passwortaussperrrichtlinie geändert removed: Passwortaussperrrichtlinie gelöscht + notification: + added: Notifikation Richtlinie hinzugefügt + changed: Notifikation Richtlinie geändert + removed: Notifikation Richtlinie entfernt flow: trigger_actions: set: Aktionen festgelegt @@ -980,7 +992,7 @@ EventTypes: removed: Instanzmitglied gelöscht cascade: removed: Instanzmitglied kaskadierend gelöscht - notification: + notification: provider: debug: fileadded: Datei zu Debug Notification Provider hinzugefügt @@ -1047,7 +1059,7 @@ EventTypes: changed: Datenschutzrichtlinie geändert security: set: Sicherheitsrichtlinie gesetzt - + removed: Instanz gelöscht secret: generator: diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 6dac00e54e..7b1e91349c 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -225,6 +225,10 @@ Errors: Empty: Org IAM Policy is empty NotExisting: Org IAM Policy doesn't exist AlreadyExists: Org IAM Policy already exists + NotificationPolicy: + NotFound: Notification Policy not found + NotChanged: Notification Policy not changed + AlreadyExists: Notification Policy already exists Project: ProjectIDMissing: Project Id missing AlreadyExists: Project already exists on organization @@ -351,6 +355,10 @@ Errors: NotExisting: Org IAM Policy not existing AlreadyExists: Org IAM Policy already exists NotChanged: Org IAM Policy has not been changed + NotificationPolicy: + NotFound: Default Notification Policy not found + NotChanged: Default Notification Policy not changed + AlreadyExists: Default Notification Policy already exists Policy: AlreadyExists: Policy already exists Label: @@ -750,6 +758,10 @@ EventTypes: added: Lockout policy added changed: Lockout policy changed removed: Lockout policy removed + notification: + added: Notification policy added + changed: Notification policy changed + removed: Notification policy removed flow: trigger_actions: set: Action set @@ -1047,7 +1059,7 @@ EventTypes: changed: Privacy policy changed security: set: Security policy set - + removed: Instance removed secret: generator: diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 9de4794226..5f6686d431 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -225,6 +225,10 @@ Errors: Empty: La politique IAM d'Org est vide NotExisting: La politique Org IAM n'existe pas AlreadyExists: La politique IAM d'Org existe déjà + NotificationPolicy: + NotFound: La politique notification n'a pas été trouvée + NotChanged: La politique notification n'a pas été modifiée + AlreadyExists: La politique notification existe déjà Project: ProjectIDMissing: Id de projet manquant AlreadyExists: Le projet existe déjà dans l'organisation @@ -351,6 +355,10 @@ Errors: NotExisting: La politique IAM d'Org n'existe pas AlreadyExists: La politique IAM d'Org existe déjà NotChanged: La politique IAM d'Org n'a pas été modifiée + NotificationPolicy: + NotFound: La politique de notification par défaut n'a pas été trouvée + NotChanged: La politique de notification par défaut n'a pas été modifiée + AlreadyExists: La ppolitique de notification par défaut existe déjà Policy: AlreadyExists: La politique existe déjà Label: @@ -725,6 +733,10 @@ EventTypes: added: Politique de confidentialité et CGU ajoutés changed: Politique de confidentialité et CGU modifiées removed: Politique de confidentialité et conditions d'utilisation supprimées + notification: + added: Politique de notification ajoutée + changed: Politique de notification modifiée + removed: Politique de notification supprimée flow: trigger_actions: set: Action set diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 0ca4d0a9dc..5ef51aac93 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -225,6 +225,10 @@ Errors: Empty: Mancano le impostazioni Org IAM NotExisting: Impostazioni Org IAM non esistenti AlreadyExists: Impostazioni Org IAM già esistenti + NotificationPolicy: + NotFound: Impostazioni di notifica non trovate + NotChanged: Impostazioni di notifica non è stato cambiato + AlreadyExists: Impostazioni di notifica già esistente Project: ProjectIDMissing: ID del progetto mancante AlreadyExists: Il progetto è già stato creato nell'organizzazione @@ -351,6 +355,10 @@ Errors: NotExisting: Impostazioni Org IAM non esistenti AlreadyExists: Impostazioni Org IAM già esistenti NotChanged: Impostazioni Org IAM non sono state cambiate + NotificationPolicy: + NotFound: Impostazioni di notifica predefinite non trovate + NotChanged: Impostazioni di notifica predefinite non è stato cambiato + AlreadyExists: Impostazioni di notifica predefinite già esistente Policy: AlreadyExists: Impostazioni già esistenti Label: @@ -725,6 +733,10 @@ EventTypes: added: Informativa sulla privacy e termini e condizioni aggiunti changed: Informativa sulla privacy e termini e condizioni cambiati removed: Informativa sulla privacy e termini e condizioni rimossi + notification: + added: Impostazione di notifica creata + changed: Impostazione di notifica cambiata + removed: Impostazione di notifica rimossa flow: trigger_actions: set: azioni salvate diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index cfd0d8c401..cfa6495861 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -225,6 +225,10 @@ Errors: Empty: 组织 IAM 策略为空 NotExisting: 组织 IAM 策略不存在 AlreadyExists: 组织 IAM 策略已存在 + NotificationPolicy: + NotFound: 未找到通知政策 + NotChanged: 通知政策没有改变 + AlreadyExists: 已经存在的通知政策 Project: ProjectIDMissing: P缺少项目 ID AlreadyExists: 项目以存在于组织中 @@ -351,6 +355,10 @@ Errors: NotExisting: 组织 IAM 策略不存在 AlreadyExists: 组织 IAM 策略已存在 NotChanged: 组织 IAM 策略未更改 + NotificationPolicy: + NotFound: 没有找到默认的通知政策 + NotChanged: 默认的通知政策没有改变 + AlreadyExists: 默认的通知政策已经存在 Policy: AlreadyExists: 策略已存在 Label: @@ -715,6 +723,10 @@ EventTypes: added: 添加隐私政策和服务条款 changed: 更改隐私政策和服务条款 removed: 删除隐私政策和服务条款 + notification: + added: 增加了通知政策 + changed: 通知政策改变 + removed: 删除了通知政策 flow: trigger_actions: set: 设置动作 diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 81cf9e35bb..4a34848ef3 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -1945,6 +1945,91 @@ service AdminService { }; } + //Add a default notification policy for ZITADEL + // it impacts all organisations without a customised policy + rpc AddNotificationPolicy(AddNotificationPolicyRequest) returns (AddNotificationPolicyResponse) { + option (google.api.http) = { + post: "/policies/notification" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "policy"; + tags: "notification policy"; + tags: "notification"; + responses: { + key: "200"; + value: { + description: "default notification policy"; + }; + }; + }; + } + + + //Returns the notification policy defined by the administrators of ZITADEL + rpc GetNotificationPolicy(GetNotificationPolicyRequest) returns (GetNotificationPolicyResponse) { + option (google.api.http) = { + get: "/policies/notification"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "policy"; + tags: "notification policy"; + tags: "notification"; + responses: { + key: "200"; + value: { + description: "default notification policy"; + }; + }; + }; + } + + //Updates the default notification policy of ZITADEL + // it impacts all organisations without a customised policy + rpc UpdateNotificationPolicy(UpdateNotificationPolicyRequest) returns (UpdateNotificationPolicyResponse) { + option (google.api.http) = { + put: "/policies/notification"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "policy"; + tags: "notification policy"; + tags: "notification"; + responses: { + key: "200"; + value: { + description: "default notification policy updated"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid argument"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + //Returns the default text for initial message (translation file) rpc GetDefaultInitMessageText(GetDefaultInitMessageTextRequest) returns (GetDefaultInitMessageTextResponse) { option (google.api.http) = { @@ -1967,10 +2052,11 @@ service AdminService { }; } + //Sets the default custom text for initial message // it impacts all organisations without customized initial message text // The Following Variables can be used: - // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} rpc SetDefaultInitMessageText(SetDefaultInitMessageTextRequest) returns (SetDefaultInitMessageTextResponse) { option (google.api.http) = { put: "/text/message/init/{language}"; @@ -2019,7 +2105,7 @@ service AdminService { //Sets the default custom text for password reset message // it impacts all organisations without customized password reset message text // The Following Variables can be used: - // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} rpc SetDefaultPasswordResetMessageText(SetDefaultPasswordResetMessageTextRequest) returns (SetDefaultPasswordResetMessageTextResponse) { option (google.api.http) = { put: "/text/message/passwordreset/{language}"; @@ -2069,7 +2155,7 @@ service AdminService { //Sets the default custom text for verify email message // it impacts all organisations without customized verify email message text // The Following Variables can be used: - // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} rpc SetDefaultVerifyEmailMessageText(SetDefaultVerifyEmailMessageTextRequest) returns (SetDefaultVerifyEmailMessageTextResponse) { option (google.api.http) = { put: "/text/message/verifyemail/{language}"; @@ -2118,7 +2204,7 @@ service AdminService { //Sets the default custom text for verify phone message // it impacts all organisations without customized verify phone message text // The Following Variables can be used: - // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} rpc SetDefaultVerifyPhoneMessageText(SetDefaultVerifyPhoneMessageTextRequest) returns (SetDefaultVerifyPhoneMessageTextResponse) { option (google.api.http) = { put: "/text/message/verifyphone/{language}"; @@ -2164,10 +2250,10 @@ service AdminService { }; } - //Sets the default custom text for domain claimed phone message + //Sets the default custom text for domain claimed message // it impacts all organisations without customized domain claimed message text // The Following Variables can be used: - // {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + // {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} rpc SetDefaultDomainClaimedMessageText(SetDefaultDomainClaimedMessageTextRequest) returns (SetDefaultDomainClaimedMessageTextResponse) { option (google.api.http) = { put: "/text/message/domainclaimed/{language}"; @@ -2216,7 +2302,7 @@ service AdminService { //Sets the default custom text for passwordless registration message // it impacts all organisations without customized passwordless registration message text // The Following Variables can be used: - // {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + // {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} rpc SetDefaultPasswordlessRegistrationMessageText(SetDefaultPasswordlessRegistrationMessageTextRequest) returns (SetDefaultPasswordlessRegistrationMessageTextResponse) { option (google.api.http) = { put: "/text/message/passwordless_registration/{language}"; @@ -2240,6 +2326,57 @@ service AdminService { }; } + + //Returns the default text for password change message (translation file) + rpc GetDefaultPasswordChangeMessageText(GetDefaultPasswordChangeMessageTextRequest) returns (GetDefaultPasswordChangeMessageTextResponse) { + option (google.api.http) = { + get: "/text/default/message/password_change/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + //Returns the custom text for password change message (overwritten in eventstore) + rpc GetCustomPasswordChangeMessageText(GetCustomPasswordChangeMessageTextRequest) returns (GetCustomPasswordChangeMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/password_change/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + //Sets the default custom text for password change message + // it impacts all organisations without customized password change message text + // The Following Variables can be used: + // {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} + rpc SetDefaultPasswordChangeMessageText(SetDefaultPasswordChangeMessageTextRequest) returns (SetDefaultPasswordChangeMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/password_change/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + }; + } + + // Removes the custom password change message text of the system + // The default text from the translation file will trigger after + rpc ResetCustomPasswordChangeMessageTextToDefault(ResetCustomPasswordChangeMessageTextToDefaultRequest) returns (ResetCustomPasswordChangeMessageTextToDefaultResponse) { + option (google.api.http) = { + delete: "/text/message/password_change/{language}" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.delete" + }; + } + + //Returns the default custom texts for login ui (translation file) rpc GetDefaultLoginTexts(GetDefaultLoginTextsRequest) returns (GetDefaultLoginTextsResponse) { option (google.api.http) = { @@ -4111,6 +4248,29 @@ message UpdatePrivacyPolicyResponse { zitadel.v1.ObjectDetails details = 1; } +message AddNotificationPolicyRequest { + bool password_change = 1; +} + +message AddNotificationPolicyResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message GetNotificationPolicyRequest {} + +message GetNotificationPolicyResponse { + zitadel.policy.v1.NotificationPolicy policy = 1; +} + +message UpdateNotificationPolicyRequest { + bool password_change = 1; +} + +message UpdateNotificationPolicyResponse { + zitadel.v1.ObjectDetails details = 1; +} + message GetDefaultInitMessageTextRequest { string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } @@ -4331,6 +4491,51 @@ message ResetCustomDomainClaimedMessageTextToDefaultResponse { zitadel.v1.ObjectDetails details = 1; } +message GetDefaultPasswordChangeMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetDefaultPasswordChangeMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message GetCustomPasswordChangeMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetCustomPasswordChangeMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetDefaultPasswordChangeMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 800}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetDefaultPasswordChangeMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message ResetCustomPasswordChangeMessageTextToDefaultRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ResetCustomPasswordChangeMessageTextToDefaultResponse { + zitadel.v1.ObjectDetails details = 1; +} + + message GetDefaultPasswordlessRegistrationMessageTextRequest { string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index fe8e95ae7a..ea4b3c8386 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2248,7 +2248,7 @@ service ManagementService { }; } - // Update the privacy complexity policy for the organisation + // Update the privacy policy for the organisation // With this policy privacy relevant things can be configured (e.g. tos link) // Variable {{.Lang}} can be set to have different links based on the language rpc UpdateCustomPrivacyPolicy(UpdateCustomPrivacyPolicyRequest) returns (UpdateCustomPrivacyPolicyResponse) { @@ -2274,6 +2274,68 @@ service ManagementService { }; } + // Returns the notification policy of the organisation + // With this notification policy it can be configured how users should be notified + rpc GetNotificationPolicy(GetNotificationPolicyRequest) returns (GetNotificationPolicyResponse) { + option (google.api.http) = { + get: "/policies/notification" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.read" + }; + } + + // Returns the default notification policy of the IAM + // With this notification privacy it can be configured how users should be notified + rpc GetDefaultNotificationPolicy(GetDefaultNotificationPolicyRequest) returns (GetDefaultNotificationPolicyResponse) { + option (google.api.http) = { + get: "/policies/default/notification" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.read" + }; + } + + // Add a custom notification policy for the organisation + // With this notification privacy it can be configured how users should be notified + rpc AddCustomNotificationPolicy(AddCustomNotificationPolicyRequest) returns (AddCustomNotificationPolicyResponse) { + option (google.api.http) = { + post: "/policies/notification" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.write" + }; + } + + // Update the notification policy for the organisation + // With this notification privacy it can be configured how users should be notified + rpc UpdateCustomNotificationPolicy(UpdateCustomNotificationPolicyRequest) returns (UpdateCustomNotificationPolicyResponse) { + option (google.api.http) = { + put: "/policies/notification" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.write" + }; + } + + // Removes the notification policy of the organisation + // The default policy of the IAM will trigger after + rpc ResetNotificationPolicyToDefault(ResetNotificationPolicyToDefaultRequest) returns (ResetNotificationPolicyToDefaultResponse) { + option (google.api.http) = { + delete: "/policies/notification" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.delete" + }; + } + // Returns the active label policy of the organisation // With this policy the private labeling can be configured (colors, etc.) rpc GetLabelPolicy(GetLabelPolicyRequest) returns (GetLabelPolicyResponse) { @@ -2487,7 +2549,7 @@ service ManagementService { // Sets the custom text for password reset message // The Following Variables can be used: - // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} rpc SetCustomPasswordResetMessageText(SetCustomPasswordResetMessageTextRequest) returns (SetCustomPasswordResetMessageTextResponse) { option (google.api.http) = { put: "/text/message/passwordreset/{language}"; @@ -2535,7 +2597,7 @@ service ManagementService { // Sets the custom text for verify email message // The Following Variables can be used: - // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} rpc SetCustomVerifyEmailMessageText(SetCustomVerifyEmailMessageTextRequest) returns (SetCustomVerifyEmailMessageTextResponse) { option (google.api.http) = { put: "/text/message/verifyemail/{language}"; @@ -2583,7 +2645,7 @@ service ManagementService { // Sets the default custom text for verify email message // The Following Variables can be used: - // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + // {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} rpc SetCustomVerifyPhoneMessageText(SetCustomVerifyPhoneMessageTextRequest) returns (SetCustomVerifyPhoneMessageTextResponse) { option (google.api.http) = { put: "/text/message/verifyphone/{language}"; @@ -2631,7 +2693,7 @@ service ManagementService { // Sets the custom text for domain claimed message // The Following Variables can be used: - // {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + // {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} rpc SetCustomDomainClaimedMessageCustomText(SetCustomDomainClaimedMessageTextRequest) returns (SetCustomDomainClaimedMessageTextResponse) { option (google.api.http) = { put: "/text/message/domainclaimed/{language}"; @@ -2679,7 +2741,7 @@ service ManagementService { // Sets the custom text for passwordless link message // The Following Variables can be used: - // {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} + // {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} rpc SetCustomPasswordlessRegistrationMessageCustomText(SetCustomPasswordlessRegistrationMessageTextRequest) returns (SetCustomPasswordlessRegistrationMessageTextResponse) { option (google.api.http) = { put: "/text/message/passwordless_registration/{language}"; @@ -2703,6 +2765,54 @@ service ManagementService { }; } + //Returns the custom text for password change message + rpc GetCustomPasswordChangeMessageText(GetCustomPasswordChangeMessageTextRequest) returns (GetCustomPasswordChangeMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/password_change/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.read"; + }; + } + + //Returns the custom text for password change link message + rpc GetDefaultPasswordChangeMessageText(GetDefaultPasswordChangeMessageTextRequest) returns (GetDefaultPasswordChangeMessageTextResponse) { + option (google.api.http) = { + get: "/text/default/message/password_change/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.read"; + }; + } + + // Sets the custom text for password change message + // The Following Variables can be used: + // {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} + rpc SetCustomPasswordChangeMessageCustomText(SetCustomPasswordChangeMessageTextRequest) returns (SetCustomPasswordChangeMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/password_change/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.write"; + }; + } + + // Removes the custom password change message text of the organisation + // The default text of the IAM will trigger after + rpc ResetCustomPasswordChangeMessageTextToDefault(ResetCustomPasswordChangeMessageTextToDefaultRequest) returns (ResetCustomPasswordChangeMessageTextToDefaultResponse) { + option (google.api.http) = { + delete: "/text/message/password_change/{language}" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.delete" + }; + } + //Returns the custom texts for login ui rpc GetCustomLoginTexts(GetCustomLoginTextsRequest) returns (GetCustomLoginTextsResponse) { option (google.api.http) = { @@ -4941,6 +5051,43 @@ message ResetPrivacyPolicyToDefaultResponse { zitadel.v1.ObjectDetails details = 1; } +//This is an empty request +message GetNotificationPolicyRequest {} + +message GetNotificationPolicyResponse { + zitadel.policy.v1.NotificationPolicy policy = 1; +} + +//This is an empty request +message GetDefaultNotificationPolicyRequest {} + +message GetDefaultNotificationPolicyResponse { + zitadel.policy.v1.NotificationPolicy policy = 1; +} + +message AddCustomNotificationPolicyRequest { + bool password_change = 1; +} + +message AddCustomNotificationPolicyResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message UpdateCustomNotificationPolicyRequest { + bool password_change = 1; +} + +message UpdateCustomNotificationPolicyResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message ResetNotificationPolicyToDefaultRequest {} + +message ResetNotificationPolicyToDefaultResponse { + zitadel.v1.ObjectDetails details = 1; +} + //This is an empty request message GetLabelPolicyRequest {} @@ -5393,6 +5540,51 @@ message ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse { zitadel.v1.ObjectDetails details = 1; } + +message GetCustomPasswordChangeMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetCustomPasswordChangeMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message GetDefaultPasswordChangeMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetDefaultPasswordChangeMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetCustomPasswordChangeMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [(validate.rules).string = {max_len: 200}]; + string pre_header = 3 [(validate.rules).string = {max_len: 200}]; + string subject = 4 [(validate.rules).string = {max_len: 200}]; + string greeting = 5 [(validate.rules).string = {max_len: 200}]; + string text = 6 [(validate.rules).string = {max_len: 800}]; + string button_text = 7 [(validate.rules).string = {max_len: 200}]; + string footer_text = 8 [(validate.rules).string = {max_len: 200}]; +} + +message SetCustomPasswordChangeMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message ResetCustomPasswordChangeMessageTextToDefaultRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ResetCustomPasswordChangeMessageTextToDefaultResponse { + zitadel.v1.ObjectDetails details = 1; +} + message GetOrgIDPByIDRequest { string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } diff --git a/proto/zitadel/policy.proto b/proto/zitadel/policy.proto index c2d0215ae7..c85308aea0 100644 --- a/proto/zitadel/policy.proto +++ b/proto/zitadel/policy.proto @@ -285,3 +285,9 @@ message PrivacyPolicy { bool is_default = 4; string help_link = 5; } + +message NotificationPolicy { + zitadel.v1.ObjectDetails details = 1; + bool is_default = 2; + bool password_change = 3; +}