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:
Livio Spring
2022-12-14 07:17:36 +01:00
committed by GitHub
parent 33e973f015
commit 632639ae7f
40 changed files with 1151 additions and 45 deletions

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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();
});
});

View File

@@ -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();
}
}
}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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',

View File

@@ -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 = [

View File

@@ -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> {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "重置设置",

View File

@@ -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);