diff --git a/packages/altair-app/angular.json b/packages/altair-app/angular.json index 2768e2e8de..878f7fa75f 100644 --- a/packages/altair-app/angular.json +++ b/packages/altair-app/angular.json @@ -69,7 +69,10 @@ "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { - "buildTarget": "altair:build:development" + "buildTarget": "altair:build:development", + "headers": { + "Content-Security-Policy": "style-src 'self' 'nonce-change-me' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='" + } }, "configurations": { "production": { @@ -123,9 +126,7 @@ } }, "cli": { - "schematicCollections": [ - "@angular-eslint/schematics" - ], + "schematicCollections": ["@angular-eslint/schematics"], "packageManager": "pnpm", "analytics": false } diff --git a/packages/altair-app/src/app/modules/altair/altair.module.ts b/packages/altair-app/src/app/modules/altair/altair.module.ts index 620409a6b3..ae7e5ce6ee 100644 --- a/packages/altair-app/src/app/modules/altair/altair.module.ts +++ b/packages/altair-app/src/app/modules/altair/altair.module.ts @@ -7,6 +7,7 @@ import { APP_INITIALIZER, ApplicationInitStatus, ModuleWithProviders, + CSP_NONCE, } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { @@ -59,6 +60,7 @@ import { RootState } from 'altair-graphql-core/build/types/state/state.interface import { AccountEffects } from './effects/account.effect'; import { WorkspaceEffects } from './effects/workspace.effect'; import { ElectronEffects } from './effects/electron.effect'; +import { AltairConfig } from 'altair-graphql-core/build/config'; registerLocaleData(en); @@ -203,6 +205,11 @@ export class AltairModule { provide: reducerToken, useValue: getReducer(), }, + { + provide: CSP_NONCE, + useFactory: (altairConfig: AltairConfig) => altairConfig.cspNonce, + deps: [AltairConfig], + }, ], }; } diff --git a/packages/altair-app/src/app/modules/altair/components/codemirror/codemirror.component.ts b/packages/altair-app/src/app/modules/altair/components/codemirror/codemirror.component.ts index 9f0024c4ab..40770eec00 100644 --- a/packages/altair-app/src/app/modules/altair/components/codemirror/codemirror.component.ts +++ b/packages/altair-app/src/app/modules/altair/components/codemirror/codemirror.component.ts @@ -50,6 +50,7 @@ import { import { tags as t } from '@lezer/highlight'; import { InternalEditorError } from '../../utils/errors'; import { debug } from '../../utils/logger'; +import { AltairConfig } from 'altair-graphql-core/build/config'; @Component({ selector: 'app-codemirror', @@ -85,7 +86,10 @@ export class CodemirrorComponent private onTouched = () => {}; private onChange = (s: string) => {}; - constructor(private zone: NgZone) {} + constructor( + private zone: NgZone, + private altairConfig: AltairConfig + ) {} ngAfterViewInit() { this.zone.runOutsideAngular(() => { @@ -407,6 +411,7 @@ export class CodemirrorComponent this.wrapLines ? EditorView.lineWrapping : [], // TODO: Create own compartment drawSelection(), EditorState.allowMultipleSelections.of(true), + EditorView.cspNonce.of(this.altairConfig.cspNonce), bracketMatching(), closeBrackets(), history(), diff --git a/packages/altair-app/src/app/modules/altair/containers/altair/altair.component.html b/packages/altair-app/src/app/modules/altair/containers/altair/altair.component.html index b401cb6b2b..b127ce9042 100644 --- a/packages/altair-app/src/app/modules/altair/containers/altair/altair.component.html +++ b/packages/altair-app/src/app/modules/altair/containers/altair/altair.component.html @@ -3,6 +3,7 @@ [appTheme]="theme$ | async" [appDarkTheme]="themeDark$ | async" [appAccentColor]="accentColor$ | async" + [cspNonce]="cspNonce" appFileDrop (fileDroppedChange)="fileDropped($event)" > diff --git a/packages/altair-app/src/app/modules/altair/containers/altair/altair.component.ts b/packages/altair-app/src/app/modules/altair/containers/altair/altair.component.ts index 6b63b443ed..1630a22781 100644 --- a/packages/altair-app/src/app/modules/altair/containers/altair/altair.component.ts +++ b/packages/altair-app/src/app/modules/altair/containers/altair/altair.component.ts @@ -121,6 +121,8 @@ export class AltairComponent { appVersion = environment.version; + cspNonce = ''; + sidebarPanels$: Observable; headerPanels$: Observable; @@ -146,6 +148,7 @@ export class AltairComponent { ) { this.isWebApp = altairConfig.isWebApp; this.authEnabled = !altairConfig.initialData.disableAccount; + this.cspNonce = altairConfig.cspNonce; this.settings$ = this.store .pipe(select('settings')) .pipe(distinctUntilChanged()); diff --git a/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.ts b/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.ts index 8fe370ec77..a38099cdf5 100644 --- a/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.ts +++ b/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.ts @@ -1,14 +1,7 @@ import { Directive, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; -import { - createTheme, - hexToRgbStr, - ICustomTheme, - ITheme, - getCSS, -} from 'altair-graphql-core/build/theme'; +import { ICustomTheme, getCSS } from 'altair-graphql-core/build/theme'; -import { css } from '@emotion/css'; -import { ThemeRegistryService } from '../../services'; +import createEmotion, { Emotion } from '@emotion/css/create-instance'; import { NzConfigService } from 'ng-zorro-antd/core/config'; @Directive({ @@ -18,7 +11,9 @@ export class ThemeDirective implements OnInit, OnChanges { @Input() appTheme: ICustomTheme = {}; @Input() appDarkTheme: ICustomTheme = {}; @Input() appAccentColor = ''; + @Input() cspNonce = ''; + private emotionInstance?: Emotion; private className = ''; constructor(private nzConfigService: NzConfigService) {} @@ -46,7 +41,9 @@ export class ThemeDirective implements OnInit, OnChanges { appDarkTheme?: ICustomTheme, accentColor?: string ) { - return css(getCSS(appTheme, appDarkTheme, accentColor)); + return this.getEmotionInstance().css( + getCSS(appTheme, appDarkTheme, accentColor) + ); } applyTheme(theme: ICustomTheme, darkTheme?: ICustomTheme, accentColor?: string) { @@ -71,4 +68,14 @@ export class ThemeDirective implements OnInit, OnChanges { this.className = this.getDynamicClassName(appTheme, appDarkTheme, accentColor); document.documentElement.classList.add(this.className); } + + private getEmotionInstance() { + if (!this.emotionInstance) { + this.emotionInstance = createEmotion({ + key: 'altair-theme', + nonce: this.cspNonce, + }); + } + return this.emotionInstance; + } } diff --git a/packages/altair-core/src/config/index.ts b/packages/altair-core/src/config/index.ts index 7cd97ba785..6237ca080d 100644 --- a/packages/altair-core/src/config/index.ts +++ b/packages/altair-core/src/config/index.ts @@ -56,6 +56,7 @@ export class AltairConfig { themes = ['light', 'dark', 'dracula', 'system']; isTranslateMode = isTranslateMode; isWebApp = (window as TODO).__ALTAIR_WEB_APP__; + cspNonce = ''; initialData = { url: '', subscriptionsEndpoint: '', @@ -105,7 +106,9 @@ export class AltairConfig { initialWindows = [], disableAccount = false, initialAuthorization, + cspNonce = 'change-me', }: AltairConfigOptions = {}) { + this.cspNonce = cspNonce; this.initialData.url = (window as TODO).__ALTAIR_ENDPOINT_URL__ ?? endpointURL ?? ''; this.initialData.subscriptionsEndpoint = diff --git a/packages/altair-core/src/config/options.ts b/packages/altair-core/src/config/options.ts index 0837c166bd..498d01060e 100644 --- a/packages/altair-core/src/config/options.ts +++ b/packages/altair-core/src/config/options.ts @@ -145,4 +145,9 @@ export interface AltairConfigOptions extends AltairWindowOptions { * Disable the account and remote syncing functionality */ disableAccount?: boolean; + + /** + * CSP nonce value to be used in Altair + */ + cspNonce?: string; } diff --git a/packages/altair-static/src/index.ts b/packages/altair-static/src/index.ts index d66f09f8b3..e706db13e9 100644 --- a/packages/altair-static/src/index.ts +++ b/packages/altair-static/src/index.ts @@ -43,6 +43,7 @@ const optionsProperties: AltairConfigOptionsObject = { persistedSettings: undefined, initialName: undefined, initialAuthorization: undefined, + cspNonce: undefined, }; const allowedProperties = Object.keys( optionsProperties