mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 21:07:22 +00:00
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 <max@caos.ch> Co-authored-by: Fabi <38692350+hifabienne@users.noreply.github.com>
This commit is contained in:
parent
33e973f015
commit
632639ae7f
@ -0,0 +1,61 @@
|
||||
<h2>{{ 'SETTINGS.LIST.SECURITY' | translate }}</h2>
|
||||
|
||||
<div class="spinner-wr">
|
||||
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div class="security-wrapper">
|
||||
<cnsl-info-section [type]="InfoSectionType.ALERT">{{ 'SETTING.SECURITY.DESCRIPTION' | translate }}</cnsl-info-section>
|
||||
|
||||
<mat-checkbox
|
||||
card-actions
|
||||
class="security-policy-toggle"
|
||||
color="primary"
|
||||
ngDefaultControl
|
||||
(change)="enabledChanged($event)"
|
||||
[(ngModel)]="enabled"
|
||||
[disabled]="(['iam.policy.write'] | hasRole | async) === false"
|
||||
>
|
||||
{{ 'SETTING.SECURITY.IFRAMEENABLED' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<form class="security-allowed-originsform" (ngSubmit)="add(redInput)">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'SETTING.SECURITY.ALLOWEDORIGINS' | translate }}</cnsl-label>
|
||||
|
||||
<input #redInput cnslInput placeholder="ex. https://" [formControl]="originsControl" />
|
||||
</cnsl-form-field>
|
||||
<button
|
||||
matTooltip="{{ 'ACTIONS.ADD' | translate }}"
|
||||
type="submit"
|
||||
mat-icon-button
|
||||
[disabled]="!enabled || originsControl.invalid || (['iam.policy.write'] | hasRole | async) === false"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="security-allowed-uris-list">
|
||||
<div *ngFor="let uri of originsList" class="uri-line" [ngClass]="{ disabled: !enabled }">
|
||||
<span class="uri">{{ uri }}</span>
|
||||
<span class="fill-space"></span>
|
||||
|
||||
<button matTooltip="{{ 'ACTIONS.DELETE' | translate }}" mat-icon-button (click)="remove(uri)" class="icon-button">
|
||||
<mat-icon class="icon">cancel</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="general-btn-container">
|
||||
<button
|
||||
class="save-button"
|
||||
(click)="savePolicy()"
|
||||
color="primary"
|
||||
type="submit"
|
||||
mat-raised-button
|
||||
[disabled]="(['iam.policy.write'] | hasRole | async) === false"
|
||||
>
|
||||
{{ 'ACTIONS.SAVE' | translate }}
|
||||
</button>
|
||||
</div>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<SecurityPolicyComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [SecurityPolicyComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SecurityPolicyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -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<SetDefaultLanguageResponse.AsObject> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -35,7 +35,9 @@
|
||||
<ng-container *ngIf="currentSetting === 'secrets' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-secret-generator></cnsl-secret-generator>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'security' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-security-policy></cnsl-security-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'branding'">
|
||||
<cnsl-private-labeling-policy [serviceType]="serviceType"></cnsl-private-labeling-policy>
|
||||
</ng-container>
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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 = [
|
||||
|
@ -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<GetSecurityPolicyResponse.AsObject> {
|
||||
const req = new GetSecurityPolicyRequest();
|
||||
return this.grpcService.admin.getSecurityPolicy(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public setSecurityPolicy(req: SetSecurityPolicyRequest): Promise<SetSecurityPolicyResponse.AsObject> {
|
||||
return this.grpcService.admin.setSecurityPolicy(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
/* notification settings */
|
||||
|
||||
public getSMTPConfig(): Promise<GetSMTPConfigResponse.AsObject> {
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": "重置设置",
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -74,3 +74,38 @@ Go to the "Advanced" section, per default login with email address should be all
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
This will change the CSP to the following:
|
||||
|
||||
```
|
||||
Content-Security-Policy: frame-ancestors https://custom-domain.com
|
||||
```
|
||||
|
||||
and remove the X-Frame-Options header.
|
||||
|
BIN
docs/static/img/guides/scenarios/security_policy.png
vendored
Normal file
BIN
docs/static/img/guides/scenarios/security_policy.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -193,3 +193,7 @@ func (m *mockInstance) RequestedDomain() string {
|
||||
func (m *mockInstance) RequestedHost() string {
|
||||
return "localhost:8080"
|
||||
}
|
||||
|
||||
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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'")
|
||||
|
@ -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)
|
||||
|
59
internal/command/instance_policy_security.go
Normal file
59
internal/command/instance_policy_security.go
Normal file
@ -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
|
||||
}
|
73
internal/command/instance_policy_security_model.go
Normal file
73
internal/command/instance_policy_security_model.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
89
internal/query/projection/security_policy.go
Normal file
89
internal/query/projection/security_policy.go
Normal file
@ -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
|
||||
}
|
98
internal/query/security_policy.go
Normal file
98
internal/query/security_policy.go
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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).
|
||||
|
80
internal/repository/instance/policy_security.go
Normal file
80
internal/repository/instance/policy_security.go
Normal file
@ -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
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user