From 632639ae7fe81db4fbabad6dd6928a39e4db0740 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 14 Dec 2022 07:17:36 +0100 Subject: [PATCH] feat: enable iframe use (#4766) * feat: enable iframe use * cleanup * fix mocks * fix linting * docs: add iframe usage to solution scenarios configurations * improve api * feat(console): security policy * description * remove unnecessary line * disable input button and urls when not enabled * add image to docs Co-authored-by: Max Peintner Co-authored-by: Fabi <38692350+hifabienne@users.noreply.github.com> --- .../security-policy.component.html | 61 ++++++++++ .../security-policy.component.scss | 97 ++++++++++++++++ .../security-policy.component.spec.ts | 24 ++++ .../security-policy.component.ts | 95 ++++++++++++++++ .../security-policy/security-policy.module.ts | 41 +++++++ .../settings-list.component.html | 4 +- .../settings-list/settings-list.module.ts | 2 + .../src/app/modules/settings-list/settings.ts | 8 ++ .../instance-settings.component.ts | 2 + console/src/app/services/admin.service.ts | 15 +++ console/src/assets/i18n/de.json | 8 +- console/src/assets/i18n/en.json | 8 +- console/src/assets/i18n/fr.json | 8 +- console/src/assets/i18n/it.json | 8 +- console/src/assets/i18n/zh.json | 8 +- console/src/component-themes.scss | 2 + docs/docs/apis/proto/admin.md | 64 +++++++++++ docs/docs/apis/proto/settings.md | 13 +++ .../solution-scenarios/configurations.mdx | 35 ++++++ .../img/guides/scenarios/security_policy.png | Bin 0 -> 40246 bytes internal/api/authz/instance.go | 5 + internal/api/authz/instance_test.go | 4 + internal/api/grpc/admin/iam_settings.go | 20 ++++ .../api/grpc/admin/iam_settings_converter.go | 8 ++ .../middleware/instance_interceptor_test.go | 4 + internal/api/http/middleware/csp.go | 77 +++++++------ .../middleware/instance_interceptor_test.go | 4 + .../api/http/middleware/security_headers.go | 8 +- internal/api/ui/console/console.go | 2 +- internal/command/instance_policy_security.go | 59 ++++++++++ .../command/instance_policy_security_model.go | 73 ++++++++++++ internal/command/main_test.go | 4 + internal/query/instance.go | 105 +++++++++++++++++- internal/query/projection/projection.go | 3 + internal/query/projection/security_policy.go | 89 +++++++++++++++ internal/query/security_policy.go | 98 ++++++++++++++++ internal/repository/instance/eventstore.go | 1 + .../repository/instance/policy_security.go | 80 +++++++++++++ proto/zitadel/admin.proto | 41 +++++++ proto/zitadel/settings.proto | 8 ++ 40 files changed, 1151 insertions(+), 45 deletions(-) create mode 100644 console/src/app/modules/policies/security-policy/security-policy.component.html create mode 100644 console/src/app/modules/policies/security-policy/security-policy.component.scss create mode 100644 console/src/app/modules/policies/security-policy/security-policy.component.spec.ts create mode 100644 console/src/app/modules/policies/security-policy/security-policy.component.ts create mode 100644 console/src/app/modules/policies/security-policy/security-policy.module.ts create mode 100644 docs/static/img/guides/scenarios/security_policy.png create mode 100644 internal/command/instance_policy_security.go create mode 100644 internal/command/instance_policy_security_model.go create mode 100644 internal/query/projection/security_policy.go create mode 100644 internal/query/security_policy.go create mode 100644 internal/repository/instance/policy_security.go diff --git a/console/src/app/modules/policies/security-policy/security-policy.component.html b/console/src/app/modules/policies/security-policy/security-policy.component.html new file mode 100644 index 000000000..29ae3d02d --- /dev/null +++ b/console/src/app/modules/policies/security-policy/security-policy.component.html @@ -0,0 +1,61 @@ +

{{ 'SETTINGS.LIST.SECURITY' | translate }}

+ +
+ +
+ +
+ {{ 'SETTING.SECURITY.DESCRIPTION' | translate }} + + + {{ 'SETTING.SECURITY.IFRAMEENABLED' | translate }} + + +
+ + {{ 'SETTING.SECURITY.ALLOWEDORIGINS' | translate }} + + + + +
+ +
+
+ {{ uri }} + + + +
+
+
+ +
+ +
diff --git a/console/src/app/modules/policies/security-policy/security-policy.component.scss b/console/src/app/modules/policies/security-policy/security-policy.component.scss new file mode 100644 index 000000000..32d12d769 --- /dev/null +++ b/console/src/app/modules/policies/security-policy/security-policy.component.scss @@ -0,0 +1,97 @@ +@use '@angular/material' as mat; + +@mixin security-policy-theme($theme) { + $foreground: map-get($theme, foreground); + $background: map-get($theme, background); + $is-dark-theme: map-get($theme, is-dark); + $warn: map-get($theme, warn); + $warn-color: map-get($warn, 500); + $button-text-color: map-get($foreground, text); + $button-disabled-text-color: map-get($foreground, disabled-button); + $divider-color: map-get($foreground, dividers); + $secondary-text: map-get($foreground, secondary-text); + + .security-wrapper { + max-width: 500px; + + .security-policy-toggle { + margin-top: 0.5rem; + } + + .security-allowed-uris-list { + width: 100%; + + .uri-line { + display: flex; + align-items: center; + margin: 0.5rem 0; + padding: 0 0 0 0.75rem; + border-radius: 4px; + background: map-get($background, infosection); + height: 30px; + box-sizing: border-box; + + .uri { + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 14px; + overflow-x: auto; + } + + .fill-space { + flex: 1; + } + + .icon-button { + height: 30px; + line-height: 30px; + + .icon { + font-size: 1rem; + margin-bottom: 3px; + } + + &:not(:hover) { + color: $secondary-text; + } + } + + &.disabled { + opacity: 0.5; + + .icon-button { + display: none; + } + } + } + } + + .security-allowed-originsform { + display: flex; + align-items: flex-end; + min-width: 320px; + + .formfield { + width: 500px; + } + + button { + margin-bottom: 14px; + margin-right: -0.5rem; + } + } + } +} + +.spinner-wr { + margin: 0.5rem 0; +} + +.general-btn-container { + display: flex; + justify-content: flex-start; + margin-top: 1rem; + + .save-button { + display: block; + } +} diff --git a/console/src/app/modules/policies/security-policy/security-policy.component.spec.ts b/console/src/app/modules/policies/security-policy/security-policy.component.spec.ts new file mode 100644 index 000000000..a2cd01624 --- /dev/null +++ b/console/src/app/modules/policies/security-policy/security-policy.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SecurityPolicyComponent } from './security-policy.component'; + +describe('SecurityPolicyComponent', () => { + let component: SecurityPolicyComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SecurityPolicyComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SecurityPolicyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/console/src/app/modules/policies/security-policy/security-policy.component.ts b/console/src/app/modules/policies/security-policy/security-policy.component.ts new file mode 100644 index 000000000..c69108346 --- /dev/null +++ b/console/src/app/modules/policies/security-policy/security-policy.component.ts @@ -0,0 +1,95 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { UntypedFormControl } from '@angular/forms'; +import { MatCheckboxChange } from '@angular/material/checkbox'; +import { SetDefaultLanguageResponse, SetSecurityPolicyRequest } from 'src/app/proto/generated/zitadel/admin_pb'; +import { AdminService } from 'src/app/services/admin.service'; +import { ToastService } from 'src/app/services/toast.service'; +import { InfoSectionType } from '../../info-section/info-section.component'; + +@Component({ + selector: 'cnsl-security-policy', + templateUrl: './security-policy.component.html', + styleUrls: ['./security-policy.component.scss'], +}) +export class SecurityPolicyComponent implements OnInit { + public originsList: string[] = []; + public enabled: boolean = false; + + public loading: boolean = false; + public InfoSectionType: any = InfoSectionType; + + @Input() public originsControl: UntypedFormControl = new UntypedFormControl({ value: [], disabled: true }); + + constructor(private service: AdminService, private toast: ToastService) {} + + ngOnInit(): void { + this.fetchData(); + } + + private fetchData(): void { + this.service.getSecurityPolicy().then((securityPolicy) => { + if (securityPolicy.policy) { + this.enabled = securityPolicy.policy?.enableIframeEmbedding; + this.originsList = securityPolicy.policy?.allowedOriginsList; + if (securityPolicy.policy.enableIframeEmbedding) { + this.originsControl.enable(); + } else { + this.originsControl.disable(); + } + } + }); + } + + private updateData(): Promise { + const req = new SetSecurityPolicyRequest(); + req.setAllowedOriginsList(this.originsList); + req.setEnableIframeEmbedding(this.enabled); + return (this.service as AdminService).setSecurityPolicy(req); + } + + public savePolicy(): void { + const prom = this.updateData(); + this.loading = true; + if (prom) { + prom + .then(() => { + this.toast.showInfo('POLICY.SECURITY_POLICY.SAVED', true); + this.loading = false; + setTimeout(() => { + this.fetchData(); + }, 2000); + }) + .catch((error) => { + this.loading = false; + this.toast.showError(error); + }); + } + } + + public add(input: any): void { + if (this.originsControl.valid) { + if (input.value !== '' && input.value !== ' ' && input.value !== '/') { + this.originsList.push(input.value); + } + if (input) { + input.value = ''; + } + } + } + + public remove(redirect: any): void { + const index = this.originsList.indexOf(redirect); + + if (index >= 0) { + this.originsList.splice(index, 1); + } + } + + public enabledChanged(event: MatCheckboxChange) { + if (event.checked) { + this.originsControl.enable(); + } else { + this.originsControl.disable(); + } + } +} diff --git a/console/src/app/modules/policies/security-policy/security-policy.module.ts b/console/src/app/modules/policies/security-policy/security-policy.module.ts new file mode 100644 index 000000000..6ec3e72b2 --- /dev/null +++ b/console/src/app/modules/policies/security-policy/security-policy.module.ts @@ -0,0 +1,41 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatIconModule } from '@angular/material/icon'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { TranslateModule } from '@ngx-translate/core'; +import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; + +import { CardModule } from '../../card/card.module'; +import { FormFieldModule } from '../../form-field/form-field.module'; +import { InfoSectionModule } from '../../info-section/info-section.module'; +import { InputModule } from '../../input/input.module'; +import { SecurityPolicyComponent } from './security-policy.component'; + +@NgModule({ + declarations: [SecurityPolicyComponent], + imports: [ + CommonModule, + CardModule, + FormsModule, + InfoSectionModule, + MatCheckboxModule, + FormsModule, + ReactiveFormsModule, + MatButtonModule, + FormFieldModule, + InputModule, + MatIconModule, + MatProgressSpinnerModule, + MatSelectModule, + HasRolePipeModule, + MatTooltipModule, + TranslateModule, + ], + exports: [SecurityPolicyComponent], +}) +export class SecurityPolicyModule {} 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 88e2da333..15097f4aa 100644 --- a/console/src/app/modules/settings-list/settings-list.component.html +++ b/console/src/app/modules/settings-list/settings-list.component.html @@ -35,7 +35,9 @@ - + + + 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 5a7f4bb39..02ff6e5b3 100644 --- a/console/src/app/modules/settings-list/settings-list.module.ts +++ b/console/src/app/modules/settings-list/settings-list.module.ts @@ -18,6 +18,7 @@ import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy import { PrivacyPolicyModule } from '../policies/privacy-policy/privacy-policy.module'; import { PrivateLabelingPolicyModule } from '../policies/private-labeling-policy/private-labeling-policy.module'; import { SecretGeneratorModule } from '../policies/secret-generator/secret-generator.module'; +import { SecurityPolicyModule } from '../policies/security-policy/security-policy.module'; import { SidenavModule } from '../sidenav/sidenav.module'; import { SettingsListComponent } from './settings-list.component'; @@ -36,6 +37,7 @@ import { SettingsListComponent } from './settings-list.component'; IdpSettingsModule, PrivacyPolicyModule, MessageTextsPolicyModule, + SecurityPolicyModule, LoginTextsPolicyModule, DomainPolicyModule, TranslateModule, diff --git a/console/src/app/modules/settings-list/settings.ts b/console/src/app/modules/settings-list/settings.ts index b890611f3..d978ce239 100644 --- a/console/src/app/modules/settings-list/settings.ts +++ b/console/src/app/modules/settings-list/settings.ts @@ -25,6 +25,14 @@ export const SECRETS: SidenavSetting = { }, }; +export const SECURITY: SidenavSetting = { + id: 'security', + i18nKey: 'SETTINGS.LIST.SECURITY', + requiredRoles: { + [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], + }, +}; + export const LOGIN: SidenavSetting = { id: 'login', i18nKey: 'SETTINGS.LIST.LOGIN', diff --git a/console/src/app/pages/instance-settings/instance-settings.component.ts b/console/src/app/pages/instance-settings/instance-settings.component.ts index b400d10ad..c55f89c71 100644 --- a/console/src/app/pages/instance-settings/instance-settings.component.ts +++ b/console/src/app/pages/instance-settings/instance-settings.component.ts @@ -19,6 +19,7 @@ import { OIDC, PRIVACYPOLICY, SECRETS, + SECURITY, } from '../../modules/settings-list/settings'; @Component({ @@ -49,6 +50,7 @@ export class InstanceSettingsComponent { PRIVACYPOLICY, OIDC, SECRETS, + SECURITY, ]; constructor(breadcrumbService: BreadcrumbService, activatedRoute: ActivatedRoute) { const breadcrumbs = [ diff --git a/console/src/app/services/admin.service.ts b/console/src/app/services/admin.service.ts index fb1c66679..35c521ff7 100644 --- a/console/src/app/services/admin.service.ts +++ b/console/src/app/services/admin.service.ts @@ -200,6 +200,10 @@ import { UpdateSMTPConfigPasswordResponse, UpdateSMTPConfigRequest, UpdateSMTPConfigResponse, + GetSecurityPolicyRequest, + GetSecurityPolicyResponse, + SetSecurityPolicyRequest, + SetSecurityPolicyResponse, } from '../proto/generated/zitadel/admin_pb'; import { SearchQuery } from '../proto/generated/zitadel/member_pb'; import { ListQuery } from '../proto/generated/zitadel/object_pb'; @@ -480,6 +484,17 @@ export class AdminService { return this.grpcService.admin.setDefaultLanguage(req, null).then((resp) => resp.toObject()); } + /* security policy */ + + public getSecurityPolicy(): Promise { + const req = new GetSecurityPolicyRequest(); + return this.grpcService.admin.getSecurityPolicy(req, null).then((resp) => resp.toObject()); + } + + public setSecurityPolicy(req: SetSecurityPolicyRequest): Promise { + return this.grpcService.admin.setSecurityPolicy(req, null).then((resp) => resp.toObject()); + } + /* notification settings */ public getSMTPConfig(): Promise { diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 52869beb6..efcf8b940 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -881,7 +881,8 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Datenschutzrichtlinie", "OIDC": "OIDC Token Lifetime und Expiration", - "SECRETS": "Secret Erscheinungsbild" + "SECRETS": "Secret Erscheinungsbild", + "SECURITY": "Sicherheitseinstellungen" }, "GROUPS": { "NOTIFICATIONS": "Benachrichtigungen", @@ -970,6 +971,11 @@ "LENGTH": "Länge", "UPDATED": "Einstellungen geändert" }, + "SECURITY": { + "DESCRIPTION": "Mit dieser Einstellung wird die CSP so eingestellt, dass Framing von einer Reihe zulässiger Domänen zugelassen wird. Beachten Sie, dass Sie durch die Aktivierung der Verwendung von iFrames das Risiko eingehen, Clickjacking zu ermöglichen.", + "IFRAMEENABLED": "iFrame zulassen", + "ALLOWEDORIGINS": "Zulässige URLs" + }, "DIALOG": { "RESET": { "DEFAULTTITLE": "Einstellungen zurücksetzen", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 2b29063c3..99e0f2392 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -881,7 +881,8 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Privacy Policy", "OIDC": "OIDC Token lifetime and expiration", - "SECRETS": "Secret Appearance" + "SECRETS": "Secret Appearance", + "SECURITY": "Security settings" }, "GROUPS": { "NOTIFICATIONS": "Notifications", @@ -970,6 +971,11 @@ "LENGTH": "Length", "UPDATED": "Settings updated." }, + "SECURITY": { + "DESCRIPTION": "This setting sets the CSP to allow framing from a set of allowed domains. Note that by enabling the use of iFrames, you run the risk of allowing clickjacking.", + "IFRAMEENABLED": "Allow iFrame", + "ALLOWEDORIGINS": "Allowed URLs" + }, "DIALOG": { "RESET": { "DEFAULTTITLE": "Reset Setting", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 90187509d..e9430565b 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -881,7 +881,8 @@ "BRANDING": "Image de marque", "PRIVACYPOLICY": "Politique de confidentialité", "OIDC": "Durée de vie et expiration des jetons OIDC", - "SECRETS": "Apparence secrète" + "SECRETS": "Apparence secrète", + "SECURITY": "Paramètres de sécurité" }, "GROUPS": { "NOTIFICATIONS": "Notifications", @@ -970,6 +971,11 @@ "LENGTH": "Longueur", "UPDATED": "Paramètres mis à jour." }, + "SECURITY": { + "DESCRIPTION": "Ce paramètre permet au CSP d'autoriser les iFrames à partir d'un ensemble de domaines autorisés. Notez qu'en autorisant l'utilisation des iFrames, vous courez le risque d'autoriser le clickjacking.", + "IFRAMEENABLED": "Autoriser iFrame", + "ALLOWEDORIGINS": "URL d'origine autorisées" + }, "DIALOG": { "RESET": { "DEFAULTTITLE": "Réinitialiser les paramètres", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 6f7f238fc..9e79ca2cc 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -881,7 +881,8 @@ "BRANDING": "Branding", "PRIVACYPOLICY": "Informativa sulla privacy e TOS", "OIDC": "OIDC Token lifetime e scadenza", - "SECRETS": "Aspetto dei segreti" + "SECRETS": "Aspetto dei segreti", + "SECURITY": "Impostazioni di sicurezza" }, "GROUPS": { "NOTIFICATIONS": "Notifiche", @@ -970,6 +971,11 @@ "LENGTH": "Lunghezza", "UPDATED": "Impostazioni aggiornati" }, + "SECURITY": { + "DESCRIPTION": "Questa impostazione consente al CSP di consentire il framing da un insieme di domini consentiti. Si noti che abilitando l'uso di iFrames, si corre il rischio di consentire il clickjacking.", + "IFRAMEENABLED": "I Frame enabled", + "ALLOWEDORIGINS": "URL consentiti" + }, "DIALOG": { "RESET": { "DEFAULTTITLE": "Ripristina impostazioni", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 036987ab9..f20857f41 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -881,7 +881,8 @@ "BRANDING": "品牌标识", "PRIVACYPOLICY": "隐私政策", "OIDC": "OIDC 令牌有效期和过期时间", - "SECRETS": "验证码外观" + "SECRETS": "验证码外观", + "SECURITY": "安全设置" }, "GROUPS": { "NOTIFICATIONS": "通知", @@ -970,6 +971,11 @@ "LENGTH": "长度", "UPDATED": "设置已更新。" }, + "SECURITY": { + "DESCRIPTION": "此设置将CSP设置为允许来自一组允许的域的框架。请注意,通过启用iFrames的使用,你会有允许点击劫持的风险。", + "IFRAMEENABLED": "允许 iFrame", + "ALLOWEDORIGINS": "允许的来源 URL" + }, "DIALOG": { "RESET": { "DEFAULTTITLE": "重置设置", diff --git a/console/src/component-themes.scss b/console/src/component-themes.scss index b377a4b0b..86e2cfaeb 100644 --- a/console/src/component-themes.scss +++ b/console/src/component-themes.scss @@ -53,6 +53,7 @@ @import 'src/app/pages/actions/add-action-dialog/add-action-dialog.component'; @import 'src/app/modules/project-role-chip/project-role-chip.component'; @import 'src/app/pages/home/home.component.scss'; +@import 'src/app/modules/policies/security-policy/security-policy.component.scss'; @import 'src/app/modules/search-user-autocomplete/search-user-autocomplete.component.scss'; @import 'src/app/modules/policies/login-policy/factor-table/factor-table.component.scss'; @import 'src/app/modules/info-overlay/info-overlay.component.scss'; @@ -71,6 +72,7 @@ @include top-view-theme($theme); @include info-overlay-theme($theme); @include app-auth-method-radio-theme($theme); + @include security-policy-theme($theme); @include search-user-autocomplete-theme($theme); @include project-role-chips-theme($theme); @include card-theme($theme); diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index d5886f291..f8f4e856a 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -332,6 +332,30 @@ Get log notification provider GET: /notification/provider/log +### GetSecurityPolicy + +> **rpc** GetSecurityPolicy([GetSecurityPolicyRequest](#getsecuritypolicyrequest)) +[GetSecurityPolicyResponse](#getsecuritypolicyresponse) + +Get the security policy + + + + GET: /policies/security + + +### SetSecurityPolicy + +> **rpc** SetSecurityPolicy([SetSecurityPolicyRequest](#setsecuritypolicyrequest)) +[SetSecurityPolicyResponse](#setsecuritypolicyresponse) + +set the security policy + + + + PUT: /policies/security + + ### GetOrgByID > **rpc** GetOrgByID([GetOrgByIDRequest](#getorgbyidrequest)) @@ -2739,6 +2763,23 @@ This is an empty request +### GetSecurityPolicyRequest +This is an empty request + + + + +### GetSecurityPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| policy | zitadel.settings.v1.SecurityPolicy | - | | + + + + ### GetSupportedLanguagesRequest This is an empty request @@ -4025,6 +4066,29 @@ this is en empty request +### SetSecurityPolicyRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| enable_iframe_embedding | bool | states if iframe embedding is enabled or disabled | | +| allowed_origins | repeated string | origins allowed to load ZITADEL in an iframe if enable_iframe_embedding is true | | + + + + +### SetSecurityPolicyResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### SetUpOrgRequest diff --git a/docs/docs/apis/proto/settings.md b/docs/docs/apis/proto/settings.md index 393118c45..c4aa2962c 100644 --- a/docs/docs/apis/proto/settings.md +++ b/docs/docs/apis/proto/settings.md @@ -106,6 +106,19 @@ title: zitadel/settings.proto +### SecurityPolicy + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | +| enable_iframe_embedding | bool | states if iframe embedding is enabled or disabled | | +| allowed_origins | repeated string | origins allowed to load ZITADEL in an iframe if enable_iframe_embedding is true | | + + + + ### TwilioConfig diff --git a/docs/docs/guides/solution-scenarios/configurations.mdx b/docs/docs/guides/solution-scenarios/configurations.mdx index 746bbc232..5cc826917 100644 --- a/docs/docs/guides/solution-scenarios/configurations.mdx +++ b/docs/docs/guides/solution-scenarios/configurations.mdx @@ -74,3 +74,38 @@ Go to the "Advanced" section, per default login with email address should be all ![Login Policy Advanced Setting: Disable email for login](/img/guides/scenarios/login_policy_advanced.png) +## Embedding ZITADEL in an iFrame + +To maximise the security during login and in the Console UI, ZITADEL follows security best practices by setting a +Content-Security-Policy (CSP) and X-Frame-Options: + +``` +Content-Security-Policy: frame-ancestors 'none' +X-Frame-Options: deny +``` + +These settings block the use of serving it in an iframe to prevents clickjacking attacks. + +### Enable iFrame embedding + +:::caution +This change can make you vulnerable to clickjacking attacks. +::: + +If your applications need to load ZITADEL inside an iframe, e.g. for a silent login or silent refresh, you can enable the use on an instance level. + +1. Navigate to the Instance Settings. +2. Click on the Security Policy tab. +3. Enable the "Allow IFrame" and add the host(s) you load the iframe from. + +You can add further hosts later on. + +![Security Settings: Allow iFrame](/img/guides/scenarios/security_policy.png) + +This will change the CSP to the following: + +``` +Content-Security-Policy: frame-ancestors https://custom-domain.com +``` + +and remove the X-Frame-Options header. diff --git a/docs/static/img/guides/scenarios/security_policy.png b/docs/static/img/guides/scenarios/security_policy.png new file mode 100644 index 0000000000000000000000000000000000000000..9783bae694a8168ca73e8c2ebc65f047ae4323ef GIT binary patch literal 40246 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 Q_Zfh|)78&qol`;+0PGfMk^lez literal 0 HcmV?d00001 diff --git a/internal/api/authz/instance.go b/internal/api/authz/instance.go index 7b79030f6..e490c4cdc 100644 --- a/internal/api/authz/instance.go +++ b/internal/api/authz/instance.go @@ -19,6 +19,7 @@ type Instance interface { RequestedHost() string DefaultLanguage() language.Tag DefaultOrganisationID() string + SecurityPolicyAllowedOrigins() []string } type InstanceVerifier interface { @@ -66,6 +67,10 @@ func (i *instance) DefaultOrganisationID() string { return i.orgID } +func (i *instance) SecurityPolicyAllowedOrigins() []string { + return nil +} + func GetInstance(ctx context.Context) Instance { instance, ok := ctx.Value(instanceKey).(Instance) if !ok { diff --git a/internal/api/authz/instance_test.go b/internal/api/authz/instance_test.go index ea06a9464..e58b34267 100644 --- a/internal/api/authz/instance_test.go +++ b/internal/api/authz/instance_test.go @@ -99,3 +99,7 @@ func (m *mockInstance) RequestedDomain() string { func (m *mockInstance) RequestedHost() string { return "zitadel.cloud:443" } + +func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { + return nil +} diff --git a/internal/api/grpc/admin/iam_settings.go b/internal/api/grpc/admin/iam_settings.go index 2e10e65a8..cfbf28563 100644 --- a/internal/api/grpc/admin/iam_settings.go +++ b/internal/api/grpc/admin/iam_settings.go @@ -106,3 +106,23 @@ func (s *Server) UpdateSMTPConfigPassword(ctx context.Context, req *admin_pb.Upd details.ResourceOwner), }, nil } + +func (s *Server) GetSecurityPolicy(ctx context.Context, req *admin_pb.GetSecurityPolicyRequest) (*admin_pb.GetSecurityPolicyResponse, error) { + policy, err := s.query.SecurityPolicy(ctx) + if err != nil { + return nil, err + } + return &admin_pb.GetSecurityPolicyResponse{ + Policy: SecurityPolicyToPb(policy), + }, nil +} + +func (s *Server) SetSecurityPolicy(ctx context.Context, req *admin_pb.SetSecurityPolicyRequest) (*admin_pb.SetSecurityPolicyResponse, error) { + details, err := s.command.SetSecurityPolicy(ctx, req.EnableIframeEmbedding, req.AllowedOrigins) + if err != nil { + return nil, err + } + return &admin_pb.SetSecurityPolicyResponse{ + Details: object.DomainToChangeDetailsPb(details), + }, nil +} diff --git a/internal/api/grpc/admin/iam_settings_converter.go b/internal/api/grpc/admin/iam_settings_converter.go index 1265a8a50..d625d16a5 100644 --- a/internal/api/grpc/admin/iam_settings_converter.go +++ b/internal/api/grpc/admin/iam_settings_converter.go @@ -149,3 +149,11 @@ func SMTPConfigToPb(smtp *query.SMTPConfig) *settings_pb.SMTPConfig { } return mapped } + +func SecurityPolicyToPb(policy *query.SecurityPolicy) *settings_pb.SecurityPolicy { + return &settings_pb.SecurityPolicy{ + Details: obj_grpc.ToViewDetailsPb(policy.Sequence, policy.CreationDate, policy.ChangeDate, policy.AggregateID), + EnableIframeEmbedding: policy.Enabled, + AllowedOrigins: policy.AllowedOrigins, + } +} diff --git a/internal/api/grpc/server/middleware/instance_interceptor_test.go b/internal/api/grpc/server/middleware/instance_interceptor_test.go index 96da6cffb..730908721 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor_test.go +++ b/internal/api/grpc/server/middleware/instance_interceptor_test.go @@ -193,3 +193,7 @@ func (m *mockInstance) RequestedDomain() string { func (m *mockInstance) RequestedHost() string { return "localhost:8080" } + +func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { + return nil +} diff --git a/internal/api/http/middleware/csp.go b/internal/api/http/middleware/csp.go index 83f69f1a4..6476d911c 100644 --- a/internal/api/http/middleware/csp.go +++ b/internal/api/http/middleware/csp.go @@ -6,36 +6,38 @@ import ( ) type CSP struct { - DefaultSrc CSPSourceOptions - ScriptSrc CSPSourceOptions - ObjectSrc CSPSourceOptions - StyleSrc CSPSourceOptions - ImgSrc CSPSourceOptions - MediaSrc CSPSourceOptions - FrameSrc CSPSourceOptions - FontSrc CSPSourceOptions - ManifestSrc CSPSourceOptions - ConnectSrc CSPSourceOptions - FormAction CSPSourceOptions + DefaultSrc CSPSourceOptions + ScriptSrc CSPSourceOptions + ObjectSrc CSPSourceOptions + StyleSrc CSPSourceOptions + ImgSrc CSPSourceOptions + MediaSrc CSPSourceOptions + FrameSrc CSPSourceOptions + FrameAncestors CSPSourceOptions + FontSrc CSPSourceOptions + ManifestSrc CSPSourceOptions + ConnectSrc CSPSourceOptions + FormAction CSPSourceOptions } var ( DefaultSCP = CSP{ - DefaultSrc: CSPSourceOptsNone(), - ScriptSrc: CSPSourceOptsSelf(), - ObjectSrc: CSPSourceOptsNone(), - StyleSrc: CSPSourceOptsSelf(), - ImgSrc: CSPSourceOptsSelf(), - MediaSrc: CSPSourceOptsNone(), - FrameSrc: CSPSourceOptsNone(), - FontSrc: CSPSourceOptsSelf(), - ManifestSrc: CSPSourceOptsSelf(), - ConnectSrc: CSPSourceOptsSelf(), + DefaultSrc: CSPSourceOptsNone(), + ScriptSrc: CSPSourceOptsSelf(), + ObjectSrc: CSPSourceOptsNone(), + StyleSrc: CSPSourceOptsSelf(), + ImgSrc: CSPSourceOptsSelf(), + MediaSrc: CSPSourceOptsNone(), + FrameSrc: CSPSourceOptsNone(), + FrameAncestors: CSPSourceOptsNone(), + FontSrc: CSPSourceOptsSelf(), + ManifestSrc: CSPSourceOptsSelf(), + ConnectSrc: CSPSourceOptsSelf(), } ) -func (csp *CSP) Value(nonce string, host string) string { - valuesMap := csp.asMap() +func (csp *CSP) Value(nonce, host string, iframe []string) string { + valuesMap := csp.asMap(iframe) values := make([]string, 0, len(valuesMap)) for k, v := range valuesMap { @@ -49,19 +51,24 @@ func (csp *CSP) Value(nonce string, host string) string { return strings.Join(values, ";") } -func (csp *CSP) asMap() map[string]CSPSourceOptions { +func (csp *CSP) asMap(iframe []string) map[string]CSPSourceOptions { + frameAncestors := csp.FrameAncestors + if len(iframe) > 0 { + frameAncestors = CSPSourceOpts().AddHost(iframe...) + } return map[string]CSPSourceOptions{ - "default-src": csp.DefaultSrc, - "script-src": csp.ScriptSrc, - "object-src": csp.ObjectSrc, - "style-src": csp.StyleSrc, - "img-src": csp.ImgSrc, - "media-src": csp.MediaSrc, - "frame-src": csp.FrameSrc, - "font-src": csp.FontSrc, - "manifest-src": csp.ManifestSrc, - "connect-src": csp.ConnectSrc, - "form-action": csp.FormAction, + "default-src": csp.DefaultSrc, + "script-src": csp.ScriptSrc, + "object-src": csp.ObjectSrc, + "style-src": csp.StyleSrc, + "img-src": csp.ImgSrc, + "media-src": csp.MediaSrc, + "frame-src": csp.FrameSrc, + "frame-ancestors": frameAncestors, + "font-src": csp.FontSrc, + "manifest-src": csp.ManifestSrc, + "connect-src": csp.ConnectSrc, + "form-action": csp.FormAction, } } diff --git a/internal/api/http/middleware/instance_interceptor_test.go b/internal/api/http/middleware/instance_interceptor_test.go index 6780b3841..40e091f67 100644 --- a/internal/api/http/middleware/instance_interceptor_test.go +++ b/internal/api/http/middleware/instance_interceptor_test.go @@ -277,3 +277,7 @@ func (m *mockInstance) RequestedDomain() string { func (m *mockInstance) RequestedHost() string { return "zitadel.cloud:443" } + +func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { + return nil +} diff --git a/internal/api/http/middleware/security_headers.go b/internal/api/http/middleware/security_headers.go index e2ff49aa4..2c058948d 100644 --- a/internal/api/http/middleware/security_headers.go +++ b/internal/api/http/middleware/security_headers.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "net/http" + "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" ) @@ -62,11 +63,14 @@ func (h *headers) ServeHTTP(w http.ResponseWriter, r *http.Request) { } r = saveContext(r, nonceKey, nonce) } + allowedHosts := authz.GetInstance(r.Context()).SecurityPolicyAllowedOrigins() headers := w.Header() - headers.Set(http_utils.ContentSecurityPolicy, h.csp.Value(nonce, r.Host)) + headers.Set(http_utils.ContentSecurityPolicy, h.csp.Value(nonce, r.Host, allowedHosts)) headers.Set(http_utils.XXSSProtection, "1; mode=block") headers.Set(http_utils.StrictTransportSecurity, "max-age=31536000; includeSubDomains") - headers.Set(http_utils.XFrameOptions, "DENY") + if len(allowedHosts) == 0 { + headers.Set(http_utils.XFrameOptions, "DENY") + } headers.Set(http_utils.XContentTypeOptions, "nosniff") headers.Set(http_utils.ReferrerPolicy, "same-origin") headers.Set(http_utils.FeaturePolicy, "payment 'none'") diff --git a/internal/api/ui/console/console.go b/internal/api/ui/console/console.go index 49a2df0e1..914b81f7d 100644 --- a/internal/api/ui/console/console.go +++ b/internal/api/ui/console/console.go @@ -99,7 +99,7 @@ func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, inst handler := mux.NewRouter() - handler.Use(security, instanceHandler) + handler.Use(instanceHandler, security) handler.Handle(envRequestPath, middleware.TelemetryHandler()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { url := http_util.BuildOrigin(r.Host, externalSecure) environmentJSON, err := createEnvironmentJSON(url, issuer(r), authz.GetInstance(r.Context()).ConsoleClientID(), customerPortal) diff --git a/internal/command/instance_policy_security.go b/internal/command/instance_policy_security.go new file mode 100644 index 000000000..c35094666 --- /dev/null +++ b/internal/command/instance_policy_security.go @@ -0,0 +1,59 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" +) + +func (c *Commands) SetSecurityPolicy(ctx context.Context, enabled bool, allowedOrigins []string) (*domain.ObjectDetails, error) { + instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) + validation := c.prepareSetSecurityPolicy(instanceAgg, enabled, allowedOrigins) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) + if err != nil { + return nil, err + } + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().InstanceID, + }, nil +} + +func (c *Commands) prepareSetSecurityPolicy(a *instance.Aggregate, enabled bool, allowedOrigins []string) preparation.Validation { + return func() (preparation.CreateCommands, error) { + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel, err := c.getSecurityPolicyWriteModel(ctx, filter) + if err != nil { + return nil, err + } + cmd, err := writeModel.NewSetEvent(ctx, &a.Aggregate, enabled, allowedOrigins) + if err != nil { + return nil, err + } + return []eventstore.Command{cmd}, nil + }, nil + } +} + +func (c *Commands) getSecurityPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (_ *InstanceSecurityPolicyWriteModel, err error) { + writeModel := NewInstanceSecurityPolicyWriteModel(ctx) + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + if len(events) == 0 { + return writeModel, nil + } + writeModel.AppendEvents(events...) + err = writeModel.Reduce() + return writeModel, err +} diff --git a/internal/command/instance_policy_security_model.go b/internal/command/instance_policy_security_model.go new file mode 100644 index 000000000..152b59014 --- /dev/null +++ b/internal/command/instance_policy_security_model.go @@ -0,0 +1,73 @@ +package command + +import ( + "context" + "reflect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" +) + +type InstanceSecurityPolicyWriteModel struct { + eventstore.WriteModel + + Enabled bool + AllowedOrigins []string +} + +func NewInstanceSecurityPolicyWriteModel(ctx context.Context) *InstanceSecurityPolicyWriteModel { + return &InstanceSecurityPolicyWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: authz.GetInstance(ctx).InstanceID(), + ResourceOwner: authz.GetInstance(ctx).InstanceID(), + }, + } +} + +func (wm *InstanceSecurityPolicyWriteModel) Reduce() error { + for _, event := range wm.Events { + if e, ok := event.(*instance.SecurityPolicySetEvent); ok { + if e.Enabled != nil { + wm.Enabled = *e.Enabled + } + if e.AllowedOrigins != nil { + wm.AllowedOrigins = *e.AllowedOrigins + } + } + } + return wm.WriteModel.Reduce() +} + +func (wm *InstanceSecurityPolicyWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(instance.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + instance.SecurityPolicySetEventType). + Builder() +} + +func (wm *InstanceSecurityPolicyWriteModel) NewSetEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + enabled bool, + allowedOrigins []string, +) (*instance.SecurityPolicySetEvent, error) { + changes := make([]instance.SecurityPolicyChanges, 0, 2) + var err error + + if wm.Enabled != enabled { + changes = append(changes, instance.ChangeSecurityPolicyEnabled(enabled)) + } + if enabled && !reflect.DeepEqual(wm.AllowedOrigins, allowedOrigins) { + changes = append(changes, instance.ChangeSecurityPolicyAllowedOrigins(allowedOrigins)) + } + changeEvent, err := instance.NewSecurityPolicySetEvent(ctx, aggregate, changes) + if err != nil { + return nil, err + } + return changeEvent, nil +} diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 697487847..b2effffbb 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -244,3 +244,7 @@ func (m *mockInstance) RequestedDomain() string { func (m *mockInstance) RequestedHost() string { return "zitadel.cloud:443" } + +func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { + return nil +} diff --git a/internal/query/instance.go b/internal/query/instance.go index ab8c8a645..09e4859e6 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -11,6 +11,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -81,6 +82,12 @@ type Instance struct { DefaultLang language.Tag Domains []*InstanceDomain host string + csp csp +} + +type csp struct { + enabled bool + allowedOrigins database.StringArray } type Instances struct { @@ -120,6 +127,13 @@ func (i *Instance) DefaultOrganisationID() string { return i.DefaultOrgID } +func (i *Instance) SecurityPolicyAllowedOrigins() []string { + if !i.csp.enabled { + return nil + } + return i.csp.allowedOrigins +} + type InstanceSearchQueries struct { SearchRequest Queries []SearchQuery @@ -189,7 +203,7 @@ func (q *Queries) InstanceByHost(ctx context.Context, host string) (_ authz.Inst ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - stmt, scan := prepareInstanceDomainQuery(host) + stmt, scan := prepareAuthzInstanceQuery(host) host = strings.Split(host, ":")[0] //remove possible port query, args, err := stmt.Where(sq.Eq{ InstanceDomainDomainCol.identifier(): host, @@ -436,3 +450,92 @@ func prepareInstanceDomainQuery(host string) (sq.SelectBuilder, func(*sql.Rows) return instance, nil } } + +func prepareAuthzInstanceQuery(host string) (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) { + return sq.Select( + InstanceColumnID.identifier(), + InstanceColumnCreationDate.identifier(), + InstanceColumnChangeDate.identifier(), + InstanceColumnSequence.identifier(), + InstanceColumnName.identifier(), + InstanceColumnDefaultOrgID.identifier(), + InstanceColumnProjectID.identifier(), + InstanceColumnConsoleID.identifier(), + InstanceColumnConsoleAppID.identifier(), + InstanceColumnDefaultLanguage.identifier(), + InstanceDomainDomainCol.identifier(), + InstanceDomainIsPrimaryCol.identifier(), + InstanceDomainIsGeneratedCol.identifier(), + InstanceDomainCreationDateCol.identifier(), + InstanceDomainChangeDateCol.identifier(), + InstanceDomainSequenceCol.identifier(), + SecurityPolicyColumnEnabled.identifier(), + SecurityPolicyColumnAllowedOrigins.identifier(), + ). + From(instanceTable.identifier()). + LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)). + LeftJoin(join(SecurityPolicyColumnInstanceID, InstanceColumnID)). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*Instance, error) { + instance := &Instance{ + host: host, + Domains: make([]*InstanceDomain, 0), + } + lang := "" + for rows.Next() { + var ( + domain sql.NullString + isPrimary sql.NullBool + isGenerated sql.NullBool + changeDate sql.NullTime + creationDate sql.NullTime + sequence sql.NullInt64 + securityPolicyEnabled sql.NullBool + ) + err := rows.Scan( + &instance.ID, + &instance.CreationDate, + &instance.ChangeDate, + &instance.Sequence, + &instance.Name, + &instance.DefaultOrgID, + &instance.IAMProjectID, + &instance.ConsoleID, + &instance.ConsoleAppID, + &lang, + &domain, + &isPrimary, + &isGenerated, + &changeDate, + &creationDate, + &sequence, + &securityPolicyEnabled, + &instance.csp.allowedOrigins, + ) + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-d3fas", "Errors.Internal") + } + if !domain.Valid { + continue + } + instance.Domains = append(instance.Domains, &InstanceDomain{ + CreationDate: creationDate.Time, + ChangeDate: changeDate.Time, + Sequence: uint64(sequence.Int64), + Domain: domain.String, + IsPrimary: isPrimary.Bool, + IsGenerated: isGenerated.Bool, + InstanceID: instance.ID, + }) + instance.csp.enabled = securityPolicyEnabled.Bool + } + if instance.ID == "" { + return nil, errors.ThrowNotFound(nil, "QUERY-n0wng", "Errors.IAM.NotFound") + } + instance.DefaultLang = language.Make(lang) + if err := rows.Close(); err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Dfbe2", "Errors.Query.CloseRows") + } + return instance, nil + } +} diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 27c1bc1c1..a5b5fda15 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -59,6 +59,7 @@ var ( OIDCSettingsProjection *oidcSettingsProjection DebugNotificationProviderProjection *debugNotificationProviderProjection KeyProjection *keyProjection + SecurityPolicyProjection *securityPolicyProjection NotificationsProjection interface{} ) @@ -131,6 +132,7 @@ func Create(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, c OIDCSettingsProjection = newOIDCSettingsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["oidc_settings"])) 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"])) newProjectionsList() return nil } @@ -221,5 +223,6 @@ func newProjectionsList() { OIDCSettingsProjection, DebugNotificationProviderProjection, KeyProjection, + SecurityPolicyProjection, } } diff --git a/internal/query/projection/security_policy.go b/internal/query/projection/security_policy.go new file mode 100644 index 000000000..57968aa4b --- /dev/null +++ b/internal/query/projection/security_policy.go @@ -0,0 +1,89 @@ +package projection + +import ( + "context" + + "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" +) + +const ( + SecurityPolicyProjectionTable = "projections.security_policies" + SecurityPolicyColumnInstanceID = "instance_id" + SecurityPolicyColumnCreationDate = "creation_date" + SecurityPolicyColumnChangeDate = "change_date" + SecurityPolicyColumnSequence = "sequence" + SecurityPolicyColumnEnabled = "enabled" + SecurityPolicyColumnAllowedOrigins = "origins" +) + +type securityPolicyProjection struct { + crdb.StatementHandler +} + +func newSecurityPolicyProjection(ctx context.Context, config crdb.StatementHandlerConfig) *securityPolicyProjection { + p := new(securityPolicyProjection) + config.ProjectionName = SecurityPolicyProjectionTable + config.Reducers = p.reducers() + config.InitCheck = crdb.NewTableCheck( + crdb.NewTable([]*crdb.Column{ + crdb.NewColumn(SecurityPolicyColumnCreationDate, crdb.ColumnTypeTimestamp), + crdb.NewColumn(SecurityPolicyColumnChangeDate, crdb.ColumnTypeTimestamp), + crdb.NewColumn(SecurityPolicyColumnInstanceID, crdb.ColumnTypeText), + crdb.NewColumn(SecurityPolicyColumnSequence, crdb.ColumnTypeInt64), + crdb.NewColumn(SecurityPolicyColumnEnabled, crdb.ColumnTypeBool, crdb.Default(false)), + crdb.NewColumn(SecurityPolicyColumnAllowedOrigins, crdb.ColumnTypeTextArray, crdb.Nullable()), + }, + crdb.NewPrimaryKey(SecurityPolicyColumnInstanceID), + ), + ) + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *securityPolicyProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: instance.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: instance.SecurityPolicySetEventType, + Reduce: p.reduceSecurityPolicySet, + }, + { + Event: instance.InstanceRemovedEventType, + Reduce: reduceInstanceRemovedHelper(SecurityPolicyColumnInstanceID), + }, + }, + }, + } +} + +func (p *securityPolicyProjection) reduceSecurityPolicySet(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*instance.SecurityPolicySetEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-D3g87", "reduce.wrong.event.type %s", instance.SecurityPolicySetEventType) + } + changes := []handler.Column{ + handler.NewCol(SecurityPolicyColumnCreationDate, e.CreationDate()), + handler.NewCol(SecurityPolicyColumnChangeDate, e.CreationDate()), + handler.NewCol(SecurityPolicyColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SecurityPolicyColumnSequence, e.Sequence()), + } + if e.Enabled != nil { + changes = append(changes, handler.NewCol(SecurityPolicyColumnEnabled, *e.Enabled)) + } + if e.AllowedOrigins != nil { + changes = append(changes, handler.NewCol(SecurityPolicyColumnAllowedOrigins, e.AllowedOrigins)) + } + return crdb.NewUpsertStatement( + e, + []handler.Column{ + handler.NewCol(SecurityPolicyColumnInstanceID, ""), + }, + changes, + ), nil +} diff --git a/internal/query/security_policy.go b/internal/query/security_policy.go new file mode 100644 index 000000000..69df79201 --- /dev/null +++ b/internal/query/security_policy.go @@ -0,0 +1,98 @@ +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/database" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/query/projection" +) + +var ( + securityPolicyTable = table{ + name: projection.SecurityPolicyProjectionTable, + instanceIDCol: projection.SecurityPolicyColumnInstanceID, + } + SecurityPolicyColumnCreationDate = Column{ + name: projection.SecurityPolicyColumnCreationDate, + table: securityPolicyTable, + } + SecurityPolicyColumnChangeDate = Column{ + name: projection.SecurityPolicyColumnChangeDate, + table: securityPolicyTable, + } + SecurityPolicyColumnInstanceID = Column{ + name: projection.SecurityPolicyColumnInstanceID, + table: securityPolicyTable, + } + SecurityPolicyColumnSequence = Column{ + name: projection.SecurityPolicyColumnSequence, + table: securityPolicyTable, + } + SecurityPolicyColumnEnabled = Column{ + name: projection.SecurityPolicyColumnEnabled, + table: securityPolicyTable, + } + SecurityPolicyColumnAllowedOrigins = Column{ + name: projection.SecurityPolicyColumnAllowedOrigins, + table: securityPolicyTable, + } +) + +type SecurityPolicy struct { + AggregateID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + Sequence uint64 + + Enabled bool + AllowedOrigins database.StringArray +} + +func (q *Queries) SecurityPolicy(ctx context.Context) (*SecurityPolicy, error) { + stmt, scan := prepareSecurityPolicyQuery() + query, args, err := stmt.Where(sq.Eq{ + SecurityPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Sf6d1", "Errors.Query.SQLStatment") + } + + row := q.client.QueryRowContext(ctx, query, args...) + return scan(row) +} + +func prepareSecurityPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*SecurityPolicy, error)) { + return sq.Select( + SecurityPolicyColumnInstanceID.identifier(), + SecurityPolicyColumnCreationDate.identifier(), + SecurityPolicyColumnChangeDate.identifier(), + SecurityPolicyColumnInstanceID.identifier(), + SecurityPolicyColumnSequence.identifier(), + SecurityPolicyColumnEnabled.identifier(), + SecurityPolicyColumnAllowedOrigins.identifier()). + From(securityPolicyTable.identifier()).PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*SecurityPolicy, error) { + securityPolicy := new(SecurityPolicy) + err := row.Scan( + &securityPolicy.AggregateID, + &securityPolicy.CreationDate, + &securityPolicy.ChangeDate, + &securityPolicy.ResourceOwner, + &securityPolicy.Sequence, + &securityPolicy.Enabled, + &securityPolicy.AllowedOrigins, + ) + if err != nil && !errs.Is(err, sql.ErrNoRows) { // ignore not found errors + return nil, errors.ThrowInternal(err, "QUERY-Dfrt2", "Errors.Internal") + } + return securityPolicy, nil + } +} diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index 1365a67eb..606a1acd5 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -30,6 +30,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(DebugNotificationProviderLogRemovedEventType, DebugNotificationProviderLogRemovedEventMapper). RegisterFilterEventMapper(OIDCSettingsAddedEventType, OIDCSettingsAddedEventMapper). RegisterFilterEventMapper(OIDCSettingsChangedEventType, OIDCSettingsChangedEventMapper). + RegisterFilterEventMapper(SecurityPolicySetEventType, SecurityPolicySetEventMapper). RegisterFilterEventMapper(LabelPolicyAddedEventType, LabelPolicyAddedEventMapper). RegisterFilterEventMapper(LabelPolicyChangedEventType, LabelPolicyChangedEventMapper). RegisterFilterEventMapper(LabelPolicyActivatedEventType, LabelPolicyActivatedEventMapper). diff --git a/internal/repository/instance/policy_security.go b/internal/repository/instance/policy_security.go new file mode 100644 index 000000000..255294035 --- /dev/null +++ b/internal/repository/instance/policy_security.go @@ -0,0 +1,80 @@ +package instance + +import ( + "context" + "encoding/json" + + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" +) + +const ( + securityPolicyPrefix = "policy.security." + SecurityPolicySetEventType = instanceEventTypePrefix + securityPolicyPrefix + "set" +) + +type SecurityPolicySetEvent struct { + eventstore.BaseEvent `json:"-"` + + Enabled *bool `json:"enabled,omitempty"` + AllowedOrigins *[]string `json:"allowedOrigins,omitempty"` +} + +func NewSecurityPolicySetEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + changes []SecurityPolicyChanges, +) (*SecurityPolicySetEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "POLICY-EWsf3", "Errors.NoChangesFound") + } + event := &SecurityPolicySetEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + SecurityPolicySetEventType, + ), + } + for _, change := range changes { + change(event) + } + return event, nil +} + +type SecurityPolicyChanges func(event *SecurityPolicySetEvent) + +func ChangeSecurityPolicyEnabled(enabled bool) func(event *SecurityPolicySetEvent) { + return func(e *SecurityPolicySetEvent) { + e.Enabled = &enabled + } +} + +func ChangeSecurityPolicyAllowedOrigins(allowedOrigins []string) func(event *SecurityPolicySetEvent) { + return func(e *SecurityPolicySetEvent) { + if len(allowedOrigins) == 0 { + allowedOrigins = []string{} + } + e.AllowedOrigins = &allowedOrigins + } +} + +func (e *SecurityPolicySetEvent) Data() interface{} { + return e +} + +func (e *SecurityPolicySetEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func SecurityPolicySetEventMapper(event *repository.Event) (eventstore.Event, error) { + securityPolicyAdded := &SecurityPolicySetEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := json.Unmarshal(event.Data, securityPolicyAdded) + if err != nil { + return nil, errors.ThrowInternal(err, "IAM-soiwj", "unable to unmarshal oidc config added") + } + + return securityPolicyAdded, nil +} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index ded7ff7d7..cbdb8a452 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -448,6 +448,29 @@ service AdminService { }; } + // Get the security policy + rpc GetSecurityPolicy(GetSecurityPolicyRequest) returns (GetSecurityPolicyResponse) { + option (google.api.http) = { + get: "/policies/security"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + } + + // set the security policy + rpc SetSecurityPolicy(SetSecurityPolicyRequest) returns (SetSecurityPolicyResponse) { + option (google.api.http) = { + put: "/policies/security"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + }; + } + // Returns an organisation by id rpc GetOrgByID(GetOrgByIDRequest) returns (GetOrgByIDResponse) { option (google.api.http) = { @@ -2929,6 +2952,24 @@ message UpdateOIDCSettingsResponse { zitadel.v1.ObjectDetails details = 1; } +// This is an empty request +message GetSecurityPolicyRequest{} + +message GetSecurityPolicyResponse{ + zitadel.settings.v1.SecurityPolicy policy = 1; +} + +message SetSecurityPolicyRequest{ + // states if iframe embedding is enabled or disabled + bool enable_iframe_embedding = 1; + // origins allowed to load ZITADEL in an iframe if enable_iframe_embedding is true + repeated string allowed_origins = 2; +} + +message SetSecurityPolicyResponse{ + zitadel.v1.ObjectDetails details = 1; +} + // if name or domain is already in use, org is not unique // at least one argument has to be provided message IsOrgUniqueRequest { diff --git a/proto/zitadel/settings.proto b/proto/zitadel/settings.proto index 02b98a7e9..3fff2d0b1 100644 --- a/proto/zitadel/settings.proto +++ b/proto/zitadel/settings.proto @@ -83,3 +83,11 @@ message OIDCSettings { google.protobuf.Duration refresh_token_idle_expiration = 4; google.protobuf.Duration refresh_token_expiration = 5; } + +message SecurityPolicy { + zitadel.v1.ObjectDetails details = 1; + // states if iframe embedding is enabled or disabled + bool enable_iframe_embedding = 2; + // origins allowed to load ZITADEL in an iframe if enable_iframe_embedding is true + repeated string allowed_origins = 3; +}