From d1c1a3814b596726783389da52e30d8d52100de2 Mon Sep 17 00:00:00 2001 From: mhg <73169169+Marcel-Haag@users.noreply.github.com> Date: Fri, 29 Oct 2021 14:38:05 +0200 Subject: [PATCH] feat: added header and project overview to FE and fixed keycloak test container --- security-c4po-angular/angular.json | 8 +- security-c4po-angular/jest.config.js | 2 + security-c4po-angular/package-lock.json | 6 +- security-c4po-angular/package.json | 2 +- .../src/app/app-routing.module.ts | 8 +- .../src/app/app.component.scss | 8 +- .../src/app/app.component.spec.ts | 4 +- security-c4po-angular/src/app/app.module.ts | 10 +- .../app/dashboard/dashboard-routing.module.ts | 16 - .../app/dashboard/dashboard.component.html | 1 - .../app/dashboard/dashboard.component.spec.ts | 27 - .../src/app/dashboard/dashboard.component.ts | 17 - .../src/app/dashboard/dashboard.module.ts | 17 - .../src/app/dashboard/index.ts | 2 - .../src/app/header/header.component.html | 26 +- .../src/app/header/header.component.scss | 30 +- .../src/app/header/header.component.spec.ts | 32 +- .../src/app/header/header.component.ts | 35 +- .../src/app/header/header.module.ts | 17 +- .../src/app/home/home.component.html | 17 +- .../src/app/home/home.component.ts | 28 +- .../src/app/project-overview/index.ts | 2 + .../project-overview-routing.module.ts | 20 + .../project-overview.component.html | 84 + .../project-overview.component.scss | 50 + .../project-overview.component.spec.ts | 79 + .../project-overview.component.ts | 70 + .../project-overview.module.ts | 30 + .../src/app/project-overview/project/index.ts | 2 + .../project/project-routing.module.ts | 12 + .../project/project.component.html | 6 + .../project/project.component.scss} | 0 .../project/project.component.spec.ts | 57 + .../project/project.component.ts | 17 + .../project/project.module.ts | 30 + .../src/assets/@theme/styles/_global.scss | 20 + .../src/assets/@theme/styles/_layout.scss | 6 - .../src/assets/@theme/styles/styles.scss | 4 - .../src/assets/@theme/theme.module.ts | 2 +- .../src/assets/i18n/de-DE.json | 12 + .../src/assets/i18n/en-US.json | 14 +- .../images/dashboard/project_default_logo.png | Bin 0 -> 9116 bytes .../project_default_logo_corporate.png | Bin 0 -> 11561 bytes .../images/favicons/corporate_favicon.ico | Bin 0 -> 270398 bytes .../{ => assets/images/favicons}/favicon.ico | Bin security-c4po-angular/src/favicon-c4po.png | Bin 1137 -> 0 bytes security-c4po-angular/src/index.html | 2 +- .../src/shared/guards/auth-guard.service.ts | 4 - .../shared/guards/login-guard.service.spec.ts | 40 - .../src/shared/guards/login-guard.service.ts | 73 - .../src/shared/models/custom-pipe.mode.ts | 10 + .../number-and-date-time-format.model.ts | 4 + .../src/shared/models/project.model.ts | 2 - .../pipes/date-time-format.pipe.spec.ts | 128 + .../src/shared/pipes/date-time-format.pipe.ts | 39 + .../shared/services/project.service.mock.ts | 14 + .../loading-spinner.component.html | 3 + .../loading-spinner.component.scss | 0 .../loading-spinner.component.spec.ts | 31 + .../loading-spinner.component.ts | 26 + security-c4po-angular/tsconfig.app.json | 3 + security-c4po-angular/tsconfig.json | 2 +- security-c4po-angular/tsconfig.spec.json | 2 +- security-c4po-api/build.gradle.kts | 1 - .../src/main/asciidoc/SecurityC4PO.adoc | 6 +- .../kotlin/com/securityc4po/api/BaseEntity.kt | 13 + .../api/configuration/security/Appuser.kt | 6 +- .../security/AppuserJwtAuthConverter.kt | 15 +- .../security/UserAccountDetailsService.kt | 14 + .../security/WebSecurityConfiguration.kt | 13 +- .../securityc4po/api/project/ProjectEntity.kt | 4 + .../api/project/ProjectService.kt | 4 + .../resources/application-COMPOSE.properties | 7 +- .../main/resources/application-DEV.properties | 6 +- .../resources/application-TEST.properties | 9 + .../src/main/resources/application.properties | 5 +- .../securityc4po/api/BaseContainerizedTest.kt | 13 +- .../ProjectControllerDocumentationTest.kt | 78 +- .../api/project/ProjectControllerIntTest.kt | 44 +- .../test/resources/create-keycloak-user.sh | 28 - .../test/resources/outdated_realm-export.json | 2230 ----------------- .../src/test/resources/realm-export.json | 93 +- .../src/test/resources/script_local | 8 +- .../backend/docker-compose.backend.yml | 6 - .../frontend/docker-compose.frontend.yml | 2 - .../kc/docker-compose.keycloak.yml | 2 - 86 files changed, 1126 insertions(+), 2694 deletions(-) delete mode 100644 security-c4po-angular/src/app/dashboard/dashboard-routing.module.ts delete mode 100644 security-c4po-angular/src/app/dashboard/dashboard.component.html delete mode 100644 security-c4po-angular/src/app/dashboard/dashboard.component.spec.ts delete mode 100644 security-c4po-angular/src/app/dashboard/dashboard.component.ts delete mode 100644 security-c4po-angular/src/app/dashboard/dashboard.module.ts delete mode 100644 security-c4po-angular/src/app/dashboard/index.ts create mode 100644 security-c4po-angular/src/app/project-overview/index.ts create mode 100644 security-c4po-angular/src/app/project-overview/project-overview-routing.module.ts create mode 100644 security-c4po-angular/src/app/project-overview/project-overview.component.html create mode 100644 security-c4po-angular/src/app/project-overview/project-overview.component.scss create mode 100644 security-c4po-angular/src/app/project-overview/project-overview.component.spec.ts create mode 100644 security-c4po-angular/src/app/project-overview/project-overview.component.ts create mode 100644 security-c4po-angular/src/app/project-overview/project-overview.module.ts create mode 100644 security-c4po-angular/src/app/project-overview/project/index.ts create mode 100644 security-c4po-angular/src/app/project-overview/project/project-routing.module.ts create mode 100644 security-c4po-angular/src/app/project-overview/project/project.component.html rename security-c4po-angular/src/app/{dashboard/dashboard.component.scss => project-overview/project/project.component.scss} (100%) create mode 100644 security-c4po-angular/src/app/project-overview/project/project.component.spec.ts create mode 100644 security-c4po-angular/src/app/project-overview/project/project.component.ts create mode 100644 security-c4po-angular/src/app/project-overview/project/project.module.ts create mode 100644 security-c4po-angular/src/assets/@theme/styles/_global.scss delete mode 100644 security-c4po-angular/src/assets/@theme/styles/_layout.scss create mode 100644 security-c4po-angular/src/assets/images/dashboard/project_default_logo.png create mode 100644 security-c4po-angular/src/assets/images/dashboard/project_default_logo_corporate.png create mode 100644 security-c4po-angular/src/assets/images/favicons/corporate_favicon.ico rename security-c4po-angular/src/{ => assets/images/favicons}/favicon.ico (100%) delete mode 100644 security-c4po-angular/src/favicon-c4po.png delete mode 100644 security-c4po-angular/src/shared/guards/login-guard.service.spec.ts delete mode 100644 security-c4po-angular/src/shared/guards/login-guard.service.ts create mode 100644 security-c4po-angular/src/shared/models/custom-pipe.mode.ts create mode 100644 security-c4po-angular/src/shared/models/number-and-date-time-format.model.ts create mode 100644 security-c4po-angular/src/shared/pipes/date-time-format.pipe.spec.ts create mode 100644 security-c4po-angular/src/shared/pipes/date-time-format.pipe.ts create mode 100644 security-c4po-angular/src/shared/services/project.service.mock.ts create mode 100644 security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.html create mode 100644 security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.scss create mode 100644 security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.spec.ts create mode 100644 security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.ts create mode 100644 security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/UserAccountDetailsService.kt create mode 100644 security-c4po-api/src/main/resources/application-TEST.properties delete mode 100644 security-c4po-api/src/test/resources/create-keycloak-user.sh delete mode 100644 security-c4po-api/src/test/resources/outdated_realm-export.json diff --git a/security-c4po-angular/angular.json b/security-c4po-angular/angular.json index 4368127..6eb197a 100644 --- a/security-c4po-angular/angular.json +++ b/security-c4po-angular/angular.json @@ -24,8 +24,8 @@ "tsConfig": "tsconfig.app.json", "aot": true, "assets": [ - "src/favicon.ico", - "src/favicon-c4po.ico", + "src/assets/images/favicons/favicon.ico", + "src/assets/images/favicons/corporate_favicon.ico", "src/assets" ], "styles": [ @@ -96,8 +96,8 @@ "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "assets": [ - "src/favicon.ico", - "src/favicon-c4po.ico", + "src/assets/images/favicons/favicon.ico", + "src/assets/images/favicons/corporate_favicon.ico", "src/assets" ], "styles": [ diff --git a/security-c4po-angular/jest.config.js b/security-c4po-angular/jest.config.js index 7ef195b..daca5fe 100644 --- a/security-c4po-angular/jest.config.js +++ b/security-c4po-angular/jest.config.js @@ -1,6 +1,8 @@ module.exports = { moduleNameMapper: { '@core/(.*)': '/src/app/core/$1', + '@assets/(.*)': '/src/assets/$1', + '@shared/(.*)': '/src/shared/$1' }, preset: 'jest-preset-angular', setupFilesAfterEnv: ['/setup-jest.ts'], diff --git a/security-c4po-angular/package-lock.json b/security-c4po-angular/package-lock.json index f5a7e9c..3e4d52f 100644 --- a/security-c4po-angular/package-lock.json +++ b/security-c4po-angular/package-lock.json @@ -2510,9 +2510,9 @@ "dev": true }, "@types/node": { - "version": "12.19.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.8.tgz", - "integrity": "sha512-D4k2kNi0URNBxIRCb1khTnkWNHv8KSL1owPmS/K5e5t8B2GzMReY7AsJIY1BnP5KdlgC4rj9jk2IkDMasIE7xg==", + "version": "12.20.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.33.tgz", + "integrity": "sha512-5XmYX2GECSa+CxMYaFsr2mrql71Q4EvHjKS+ox/SiwSdaASMoBIWE6UmZqFO+VX1jIcsYLStI4FFoB6V7FeIYw==", "dev": true }, "@types/normalize-package-data": { diff --git a/security-c4po-angular/package.json b/security-c4po-angular/package.json index dda4d09..f623976 100644 --- a/security-c4po-angular/package.json +++ b/security-c4po-angular/package.json @@ -58,7 +58,7 @@ "@types/jasmine": "~3.5.0", "@types/jasminewd2": "~2.0.3", "@types/jest": "26.0.15", - "@types/node": "^12.19.8", + "@types/node": "^12.20.33", "codelyzer": "^6.0.0", "font-awesome": "^4.7.0", "jasmine-core": "~3.6.0", diff --git a/security-c4po-angular/src/app/app-routing.module.ts b/security-c4po-angular/src/app/app-routing.module.ts index b84f70a..7f29841 100644 --- a/security-c4po-angular/src/app/app-routing.module.ts +++ b/security-c4po-angular/src/app/app-routing.module.ts @@ -3,7 +3,7 @@ import { Routes, RouterModule } from '@angular/router'; import {HomeComponent} from './home/home.component'; import {AuthGuardService} from '../shared/guards/auth-guard.service'; -export const START_PAGE = 'home'; +export const START_PAGE = 'projects'; const routes: Routes = [ { @@ -12,11 +12,11 @@ const routes: Routes = [ canActivate: [AuthGuardService] }, { - path: 'dashboard', - loadChildren: () => import('./dashboard').then(mod => mod.DashboardModule), + path: 'projects', + loadChildren: () => import('./project-overview').then(mod => mod.ProjectOverviewModule), canActivate: [AuthGuardService] }, - // ToDo: Exchange default Keycloak login with self made login + // ToDo: Remove after default Keycloak login mask got reworked /*{ path: 'login', loadChildren: () => import('./login').then(mod => mod.LoginModule), diff --git a/security-c4po-angular/src/app/app.component.scss b/security-c4po-angular/src/app/app.component.scss index 2048294..31f7946 100644 --- a/security-c4po-angular/src/app/app.component.scss +++ b/security-c4po-angular/src/app/app.component.scss @@ -1,15 +1,9 @@ @import "../assets/@theme/styles/_variables.scss"; .content-container { - width: 90vw; - height: calc(90vh - #{$header-height}); .scrollable-content { - width: 100%; - max-width: 100vw; - height: 100%; - max-height: calc(100vh - #{$header-height}); - overflow: auto; } + } diff --git a/security-c4po-angular/src/app/app.component.spec.ts b/security-c4po-angular/src/app/app.component.spec.ts index 889ebdf..6fa8739 100644 --- a/security-c4po-angular/src/app/app.component.spec.ts +++ b/security-c4po-angular/src/app/app.component.spec.ts @@ -6,9 +6,9 @@ import {NbEvaIconsModule} from '@nebular/eva-icons'; import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; import {HttpLoaderFactory} from './common-app.module'; import {HttpClient} from '@angular/common/http'; -import {ThemeModule} from '../assets/@theme/theme.module'; +import {ThemeModule} from '@assets/@theme/theme.module'; import {HttpClientTestingModule} from '@angular/common/http/testing'; -import {SessionState} from '../shared/stores/session-state/session-state'; +import {SessionState} from '@shared/stores/session-state/session-state'; import {NgxsModule} from '@ngxs/store'; import {HeaderModule} from './header/header.module'; import {KeycloakService} from 'keycloak-angular'; diff --git a/security-c4po-angular/src/app/app.module.ts b/security-c4po-angular/src/app/app.module.ts index 2ce09d6..e3bc9bc 100644 --- a/security-c4po-angular/src/app/app.module.ts +++ b/security-c4po-angular/src/app/app.module.ts @@ -17,14 +17,15 @@ import {FaConfig, FaIconLibrary, FontAwesomeModule} from '@fortawesome/angular-f import {fas} from '@fortawesome/free-solid-svg-icons'; import {far} from '@fortawesome/free-regular-svg-icons'; import {NgxsModule} from '@ngxs/store'; -import {SessionState} from '../shared/stores/session-state/session-state'; +import {SessionState} from '@shared/stores/session-state/session-state'; import {environment} from '../environments/environment'; -import {NotificationService} from '../shared/services/notification.service'; +import {NotificationService} from '@shared/services/notification.service'; import {ThemeModule} from '@assets/@theme/theme.module'; import {HeaderModule} from './header/header.module'; import {HomeModule} from './home/home.module'; import {KeycloakService} from 'keycloak-angular'; -import {httpInterceptorProviders} from '../shared/interceptors'; +import {httpInterceptorProviders} from '@shared/interceptors'; +import {FlexLayoutModule} from '@angular/flex-layout'; @NgModule({ declarations: [ @@ -53,7 +54,8 @@ import {httpInterceptorProviders} from '../shared/interceptors'; } }), HeaderModule, - HomeModule + HomeModule, + FlexLayoutModule ], providers: [ HttpClient, diff --git a/security-c4po-angular/src/app/dashboard/dashboard-routing.module.ts b/security-c4po-angular/src/app/dashboard/dashboard-routing.module.ts deleted file mode 100644 index cffddf6..0000000 --- a/security-c4po-angular/src/app/dashboard/dashboard-routing.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; -import {DashboardComponent} from './dashboard.component'; - -const routes: Routes = [ - { - path: '', - component: DashboardComponent - } -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) -export class DashboardRoutingModule { } diff --git a/security-c4po-angular/src/app/dashboard/dashboard.component.html b/security-c4po-angular/src/app/dashboard/dashboard.component.html deleted file mode 100644 index 9c5fce9..0000000 --- a/security-c4po-angular/src/app/dashboard/dashboard.component.html +++ /dev/null @@ -1 +0,0 @@ -

dashboard works!

diff --git a/security-c4po-angular/src/app/dashboard/dashboard.component.spec.ts b/security-c4po-angular/src/app/dashboard/dashboard.component.spec.ts deleted file mode 100644 index a83926d..0000000 --- a/security-c4po-angular/src/app/dashboard/dashboard.component.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DashboardComponent } from './dashboard.component'; - -describe('DashboardComponent', () => { - let component: DashboardComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ - DashboardComponent - ] - }) - .compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DashboardComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/security-c4po-angular/src/app/dashboard/dashboard.component.ts b/security-c4po-angular/src/app/dashboard/dashboard.component.ts deleted file mode 100644 index fbe68b4..0000000 --- a/security-c4po-angular/src/app/dashboard/dashboard.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {Component, OnDestroy, OnInit} from '@angular/core'; - -@Component({ - selector: 'app-dashboard', - templateUrl: './dashboard.component.html', - styleUrls: ['./dashboard.component.scss'] -}) -export class DashboardComponent implements OnInit, OnDestroy { - - constructor() { } - - ngOnInit(): void { - } - - ngOnDestroy(): void { - } -} diff --git a/security-c4po-angular/src/app/dashboard/dashboard.module.ts b/security-c4po-angular/src/app/dashboard/dashboard.module.ts deleted file mode 100644 index 43913fa..0000000 --- a/security-c4po-angular/src/app/dashboard/dashboard.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {DashboardComponent} from './dashboard.component'; - -@NgModule({ - declarations: [ - DashboardComponent, - ], - exports: [ - DashboardComponent - ], - imports: [ - CommonModule - ] -}) -export class DashboardModule { -} diff --git a/security-c4po-angular/src/app/dashboard/index.ts b/security-c4po-angular/src/app/dashboard/index.ts deleted file mode 100644 index 87d1201..0000000 --- a/security-c4po-angular/src/app/dashboard/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {DashboardModule} from './dashboard.module'; -export {DashboardRoutingModule} from './dashboard-routing.module'; diff --git a/security-c4po-angular/src/app/header/header.component.html b/security-c4po-angular/src/app/header/header.component.html index eff6a91..88d7422 100644 --- a/security-c4po-angular/src/app/header/header.component.html +++ b/security-c4po-angular/src/app/header/header.component.html @@ -1,4 +1,26 @@ -
-

header works!

+
+ logo dark + + logo light + + +
+

{{SECURITYC4PO_TITLE}}

+
+
+ + + + + +
diff --git a/security-c4po-angular/src/app/header/header.component.scss b/security-c4po-angular/src/app/header/header.component.scss index 9792749..821592f 100644 --- a/security-c4po-angular/src/app/header/header.component.scss +++ b/security-c4po-angular/src/app/header/header.component.scss @@ -1,9 +1,31 @@ + +@import '~@nebular/theme/styles/global/breakpoints'; @import "../../assets/@theme/styles/_variables.scss"; -.c4po-header { - height: $header-height; +@mixin nb-overrides { + display: inline-flex; + justify-content: space-between; + width: 100%; - .toggle-dark-mode { - text-align: right; + .header { + display: flex; + align-items: center; + width: auto; + + .logo-container { + font-style: oblique; + color: #e74c3c; + } + + nb-action { + height: auto; + display: flex; + align-content: center; + } + + .toggle-theme { + align-content: flex-end; + display: flex; + } } } diff --git a/security-c4po-angular/src/app/header/header.component.spec.ts b/security-c4po-angular/src/app/header/header.component.spec.ts index 381e8e8..b5e5bb4 100644 --- a/security-c4po-angular/src/app/header/header.component.spec.ts +++ b/security-c4po-angular/src/app/header/header.component.spec.ts @@ -1,6 +1,14 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; -import { HeaderComponent } from './header.component'; +import {HeaderComponent} from './header.component'; +import {CommonModule} from '@angular/common'; +import {FontAwesomeTestingModule} from '@fortawesome/angular-fontawesome/testing'; +import {NbActionsModule} from '@nebular/theme'; +import {ThemeModule} from '@assets/@theme/theme.module'; +import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import {HttpLoaderFactory} from '../common-app.module'; +import {HttpClient} from '@angular/common/http'; +import {RouterTestingModule} from '@angular/router/testing'; describe('HeaderComponent', () => { let component: HeaderComponent; @@ -8,9 +16,25 @@ describe('HeaderComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ HeaderComponent ] + declarations: [ + HeaderComponent + ], + imports: [ + CommonModule, + NbActionsModule, + FontAwesomeTestingModule, + ThemeModule.forRoot(), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }), + RouterTestingModule.withRoutes([]) + ] }) - .compileComponents(); + .compileComponents(); }); beforeEach(() => { diff --git a/security-c4po-angular/src/app/header/header.component.ts b/security-c4po-angular/src/app/header/header.component.ts index 59874a9..83d5485 100644 --- a/security-c4po-angular/src/app/header/header.component.ts +++ b/security-c4po-angular/src/app/header/header.component.ts @@ -1,14 +1,43 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import * as FA from '@fortawesome/free-solid-svg-icons'; +import {NbThemeService} from '@nebular/theme'; +import {map} from 'rxjs/operators'; +import {untilDestroyed} from 'ngx-take-until-destroy'; +import {GlobalTitlesVariables} from '@shared/config/global-variables'; + @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.scss'] }) -export class HeaderComponent implements OnInit { +export class HeaderComponent implements OnInit, OnDestroy { - constructor() { } + readonly fa = FA; + readonly SECURITYC4PO_TITLE = GlobalTitlesVariables.SECURITYC4PO_TITLE; + + currentTheme = ''; + + constructor(private themeService: NbThemeService) { } ngOnInit(): void { + this.themeService.onThemeChange() + .pipe( + map(({ name }) => name), + untilDestroyed(this), + ) + .subscribe(themeName => this.currentTheme = themeName); } + onClickSwitchTheme(): void { + if (this.currentTheme === 'corporate') { + this.themeService.changeTheme('dark'); + } else if (this.currentTheme === 'dark') { + this.themeService.changeTheme('corporate'); + } + } + + ngOnDestroy(): void { + // This method must be present when using ngx-take-until-destroy + // even when empty + } } diff --git a/security-c4po-angular/src/app/header/header.module.ts b/security-c4po-angular/src/app/header/header.module.ts index ebaf0a3..2c0aeb6 100644 --- a/security-c4po-angular/src/app/header/header.module.ts +++ b/security-c4po-angular/src/app/header/header.module.ts @@ -1,6 +1,9 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; import {HeaderComponent} from './header.component'; +import {NbActionsModule, NbButtonModule, NbCardModule} from '@nebular/theme'; +import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; +import {FlexLayoutModule} from '@angular/flex-layout'; @NgModule({ declarations: [ @@ -10,7 +13,13 @@ import {HeaderComponent} from './header.component'; HeaderComponent ], imports: [ - CommonModule + CommonModule, + NbButtonModule, + FontAwesomeModule, + NbCardModule, + NbActionsModule, + FlexLayoutModule ] }) -export class HeaderModule { } +export class HeaderModule { +} diff --git a/security-c4po-angular/src/app/home/home.component.html b/security-c4po-angular/src/app/home/home.component.html index 379ce84..9217591 100644 --- a/security-c4po-angular/src/app/home/home.component.html +++ b/security-c4po-angular/src/app/home/home.component.html @@ -1,16 +1 @@ - - - - - - - -
- {{projects.getValue() | json}} -
- - {{'No Projects available!'}} - -
- -
+

app-home-component works...

diff --git a/security-c4po-angular/src/app/home/home.component.ts b/security-c4po-angular/src/app/home/home.component.ts index b4515fa..5e3d7fc 100644 --- a/security-c4po-angular/src/app/home/home.component.ts +++ b/security-c4po-angular/src/app/home/home.component.ts @@ -1,36 +1,14 @@ -import {Component, OnDestroy, OnInit} from '@angular/core'; -import {BehaviorSubject} from 'rxjs'; -import {Project} from '../../shared/models/project.model'; -import {ProjectService} from '../../shared/services/project.service'; -import {untilDestroyed} from 'ngx-take-until-destroy'; +import {Component, OnInit} from '@angular/core'; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) -export class HomeComponent implements OnInit, OnDestroy { +export class HomeComponent implements OnInit { - projects: BehaviorSubject = new BehaviorSubject([]); - - constructor(private projectService: ProjectService) { } + constructor() { } ngOnInit(): void { } - - onClickGetProjects(): void { - this.getProjects(); - } - - getProjects(): void { - this.projectService.getProjects() - .pipe(untilDestroyed(this)) - .subscribe((projects) => { - this.projects.next(projects); - }); - } - - ngOnDestroy(): void { - } - } diff --git a/security-c4po-angular/src/app/project-overview/index.ts b/security-c4po-angular/src/app/project-overview/index.ts new file mode 100644 index 0000000..dd66278 --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/index.ts @@ -0,0 +1,2 @@ +export {ProjectOverviewModule} from './project-overview.module'; +export {ProjectOverviewRoutingModule} from './project-overview-routing.module'; diff --git a/security-c4po-angular/src/app/project-overview/project-overview-routing.module.ts b/security-c4po-angular/src/app/project-overview/project-overview-routing.module.ts new file mode 100644 index 0000000..17ae336 --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project-overview-routing.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {ProjectOverviewComponent} from './project-overview.component'; + +const routes: Routes = [ + { + path: '', + component: ProjectOverviewComponent + }, + { + path: 'id', + loadChildren: () => import('./project').then(mod => mod.ProjectModule), + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ProjectOverviewRoutingModule { } diff --git a/security-c4po-angular/src/app/project-overview/project-overview.component.html b/security-c4po-angular/src/app/project-overview/project-overview.component.html new file mode 100644 index 0000000..41cb515 --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project-overview.component.html @@ -0,0 +1,84 @@ +
+
+ + +

{{project?.title}}

+
+ +

+ {{'project.client' | translate}}: +

+ + {{project?.client}} + + +

+ {{'project.tester' | translate}}: +

+ + {{project?.tester}} + + +

+ {{'project.createdAt' | translate}}: +

+ + {{project?.createdAt | dateTimeFormat}} + +
+ + +
+ + + + +
+
+
+
+
+ +
+

+ {{'project.overview.no.projects' | translate}} +

+
+ +
+ +
+ + + + diff --git a/security-c4po-angular/src/app/project-overview/project-overview.component.scss b/security-c4po-angular/src/app/project-overview/project-overview.component.scss new file mode 100644 index 0000000..c411834 --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project-overview.component.scss @@ -0,0 +1,50 @@ +.project-card { + max-width: 22rem; + width: 22rem; + min-width: 20rem; + max-height: 100%; + height: 100%; + min-height: 100%; + + .project-header { + max-height: 8rem; + height: 8rem; + min-height: 6rem; + } + + .project-subheader { + font-size: 1.25rem; + font-weight: bold; + } + + .project-paragraph { + font-size: 1.15rem; + font-style: italic; + } + + .project-progress { + max-width: 65%; + width: 65%; + min-width: 65%; + } + + .project-button { + height: 1.425rem; + } +} + +.project-link:hover { + cursor: pointer !important; +} + +.add-project-button { + margin: 6rem 2rem 6rem 0; + .new-project-icon { + padding-right: 0.5rem; + } +} + +.error-text { + font-size: 1.25rem; + font-weight: bold; +} diff --git a/security-c4po-angular/src/app/project-overview/project-overview.component.spec.ts b/security-c4po-angular/src/app/project-overview/project-overview.component.spec.ts new file mode 100644 index 0000000..062e6c2 --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project-overview.component.spec.ts @@ -0,0 +1,79 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ProjectOverviewComponent} from './project-overview.component'; +import {DateTimeFormatPipe} from '@shared/pipes/date-time-format.pipe'; +import {CommonModule} from '@angular/common'; +import {ProjectOverviewRoutingModule} from './project-overview-routing.module'; +import {NbButtonModule, NbCardModule, NbProgressBarModule, NbSpinnerModule} from '@nebular/theme'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; +import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import {ProjectService} from '@shared/services/project.service'; +import {HttpLoaderFactory} from '../common-app.module'; +import {HttpClient, HttpClientModule} from '@angular/common/http'; +import {RouterTestingModule} from '@angular/router/testing'; +import {NgxsModule} from '@ngxs/store'; +import {SessionState} from '@shared/stores/session-state/session-state'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {NotificationService} from '@shared/services/notification.service'; +import {NotificationServiceMock} from '@shared/services/notification.service.mock'; +import {ProjectServiceMock} from '@shared/services/project.service.mock'; +import {ThemeModule} from '@assets/@theme/theme.module'; +import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component'; +import {KeycloakService} from 'keycloak-angular'; + +describe('ProjectOverviewComponent', () => { + let component: ProjectOverviewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + ProjectOverviewComponent, + LoadingSpinnerComponent, + DateTimeFormatPipe + ], + imports: [ + CommonModule, + ProjectOverviewRoutingModule, + NbCardModule, + NbButtonModule, + FlexLayoutModule, + BrowserAnimationsModule, + FontAwesomeModule, + TranslateModule, + NbProgressBarModule, + NbSpinnerModule, + ThemeModule.forRoot(), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }), + RouterTestingModule.withRoutes([]), + NgxsModule.forRoot([SessionState]), + HttpClientModule, + HttpClientTestingModule + ], + providers: [ + KeycloakService, + {provide: ProjectService, useValue: new ProjectServiceMock()}, + {provide: NotificationService, useValue: new NotificationServiceMock()} + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProjectOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/security-c4po-angular/src/app/project-overview/project-overview.component.ts b/security-c4po-angular/src/app/project-overview/project-overview.component.ts new file mode 100644 index 0000000..f8ddbac --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project-overview.component.ts @@ -0,0 +1,70 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import * as FA from '@fortawesome/free-solid-svg-icons'; +import {Project} from '@shared/models/project.model'; +import {BehaviorSubject, Observable} from 'rxjs'; +import {untilDestroyed} from 'ngx-take-until-destroy'; +import {ProjectService} from '@shared/services/project.service'; +import {NotificationService, PopupType} from '@shared/services/notification.service'; +import {tap} from 'rxjs/operators'; + +@Component({ + selector: 'app-project-overview', + templateUrl: './project-overview.component.html', + styleUrls: ['./project-overview.component.scss'] +}) +export class ProjectOverviewComponent implements OnInit, OnDestroy { + + readonly fa = FA; + + loading$: BehaviorSubject = new BehaviorSubject(true); + projects: BehaviorSubject = new BehaviorSubject([]); + + constructor( + private readonly projectService: ProjectService, + private readonly notificationService: NotificationService) { + } + + ngOnInit(): void { + this.getProjects(); + } + + getProjects(): void { + this.projectService.getProjects() + .pipe( + untilDestroyed(this), + tap(() => this.loading$.next(true)) + ) + .subscribe( { + next: (projects) => { + this.projects.next(projects); + this.loading$.next(false); + }, + error: err => { + console.log(err); + this.notificationService.showPopup('project.popup.not.found', PopupType.FAILURE); + this.loading$.next(false); + } + }); + } + + onClickAddProject(): void { + console.log('to be implemented...'); + } + + onClickEditProject(): void { + console.log('to be implemented...'); + } + + onClickDeleteProject(): void { + console.log('to be implemented...'); + } + + isLoading(): Observable { + return this.loading$.asObservable(); + } + + ngOnDestroy(): void { + // This method must be present when using ngx-take-until-destroy + // even when empty + } +} diff --git a/security-c4po-angular/src/app/project-overview/project-overview.module.ts b/security-c4po-angular/src/app/project-overview/project-overview.module.ts new file mode 100644 index 0000000..c8ac3b5 --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project-overview.module.ts @@ -0,0 +1,30 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ProjectOverviewComponent} from './project-overview.component'; +import {ProjectOverviewRoutingModule} from './project-overview-routing.module'; +import {NbButtonModule, NbCardModule, NbProgressBarModule} from '@nebular/theme'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; +import {TranslateModule} from '@ngx-translate/core'; +import {DateTimeFormatPipe} from '@shared/pipes/date-time-format.pipe'; +import {ProjectModule} from './project'; + +@NgModule({ + declarations: [ + ProjectOverviewComponent, + DateTimeFormatPipe + ], + imports: [ + CommonModule, + ProjectOverviewRoutingModule, + NbCardModule, + NbButtonModule, + FlexLayoutModule, + FontAwesomeModule, + TranslateModule, + NbProgressBarModule, + ProjectModule + ] +}) +export class ProjectOverviewModule { +} diff --git a/security-c4po-angular/src/app/project-overview/project/index.ts b/security-c4po-angular/src/app/project-overview/project/index.ts new file mode 100644 index 0000000..c212fb4 --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project/index.ts @@ -0,0 +1,2 @@ +export {ProjectModule} from './project.module'; +export {ProjectRoutingModule} from './project-routing.module'; diff --git a/security-c4po-angular/src/app/project-overview/project/project-routing.module.ts b/security-c4po-angular/src/app/project-overview/project/project-routing.module.ts new file mode 100644 index 0000000..c5444ca --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project/project-routing.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + + + +@NgModule({ + declarations: [], + imports: [ + CommonModule + ] +}) +export class ProjectRoutingModule { } diff --git a/security-c4po-angular/src/app/project-overview/project/project.component.html b/security-c4po-angular/src/app/project-overview/project/project.component.html new file mode 100644 index 0000000..000715f --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project/project.component.html @@ -0,0 +1,6 @@ + + +

{{selectedProjectTitle}} works!

+
+
+ diff --git a/security-c4po-angular/src/app/dashboard/dashboard.component.scss b/security-c4po-angular/src/app/project-overview/project/project.component.scss similarity index 100% rename from security-c4po-angular/src/app/dashboard/dashboard.component.scss rename to security-c4po-angular/src/app/project-overview/project/project.component.scss diff --git a/security-c4po-angular/src/app/project-overview/project/project.component.spec.ts b/security-c4po-angular/src/app/project-overview/project/project.component.spec.ts new file mode 100644 index 0000000..fcd9d17 --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project/project.component.spec.ts @@ -0,0 +1,57 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ProjectComponent} from './project.component'; +import {CommonModule} from '@angular/common'; +import {ThemeModule} from '@assets/@theme/theme.module'; +import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import {HttpLoaderFactory} from '../../common-app.module'; +import {HttpClient, HttpClientModule} from '@angular/common/http'; +import {RouterTestingModule} from '@angular/router/testing'; +import {NgxsModule} from '@ngxs/store'; +import {SessionState} from '@shared/stores/session-state/session-state'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NbLayoutModule} from '@nebular/theme'; +import {KeycloakService} from 'keycloak-angular'; + +describe('ProjectComponent', () => { + let component: ProjectComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + ProjectComponent + ], + imports: [ + CommonModule, + NbLayoutModule, + ThemeModule.forRoot(), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }), + RouterTestingModule.withRoutes([]), + NgxsModule.forRoot([SessionState]), + HttpClientModule, + HttpClientTestingModule + ], + providers: [ + KeycloakService + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProjectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/security-c4po-angular/src/app/project-overview/project/project.component.ts b/security-c4po-angular/src/app/project-overview/project/project.component.ts new file mode 100644 index 0000000..47fa0a4 --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project/project.component.ts @@ -0,0 +1,17 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-project', + templateUrl: './project.component.html', + styleUrls: ['./project.component.scss'] +}) +export class ProjectComponent implements OnInit { + + selectedProjectTitle: string = history?.state?.selectedProject ? history?.state?.selectedProject.title : ''; + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/security-c4po-angular/src/app/project-overview/project/project.module.ts b/security-c4po-angular/src/app/project-overview/project/project.module.ts new file mode 100644 index 0000000..c26c1ab --- /dev/null +++ b/security-c4po-angular/src/app/project-overview/project/project.module.ts @@ -0,0 +1,30 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {RouterModule} from '@angular/router'; +import {ProjectComponent} from './project.component'; +import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component'; +import {NbCardModule, NbLayoutModule, NbSpinnerModule} from '@nebular/theme'; +import {FlexLayoutModule} from '@angular/flex-layout'; + +@NgModule({ + declarations: [ + ProjectComponent, + LoadingSpinnerComponent + ], + exports: [ + LoadingSpinnerComponent + ], + imports: [ + CommonModule, + RouterModule.forChild([{ + path: '', + component: ProjectComponent + }]), + NbCardModule, + NbSpinnerModule, + FlexLayoutModule, + NbLayoutModule, + ] +}) +export class ProjectModule { +} diff --git a/security-c4po-angular/src/assets/@theme/styles/_global.scss b/security-c4po-angular/src/assets/@theme/styles/_global.scss new file mode 100644 index 0000000..5ec1b7b --- /dev/null +++ b/security-c4po-angular/src/assets/@theme/styles/_global.scss @@ -0,0 +1,20 @@ +@import "_variables.scss"; + +nb-layout { + width: 100vw; + height: 100vh; + + nb-layout-header { + max-width: 100vw; + height: $header-height; + + justify-content: center; + } + + nb-layout-column { + width: 100vw; + height: calc(100vh - #{$header-height}); + + overflow: auto; + } +} diff --git a/security-c4po-angular/src/assets/@theme/styles/_layout.scss b/security-c4po-angular/src/assets/@theme/styles/_layout.scss deleted file mode 100644 index fc32414..0000000 --- a/security-c4po-angular/src/assets/@theme/styles/_layout.scss +++ /dev/null @@ -1,6 +0,0 @@ -@mixin ngx-layout() { - @include media-breakpoint-down(is) { - .row { - } - } -} diff --git a/security-c4po-angular/src/assets/@theme/styles/styles.scss b/security-c4po-angular/src/assets/@theme/styles/styles.scss index ec7e63c..741f677 100644 --- a/security-c4po-angular/src/assets/@theme/styles/styles.scss +++ b/security-c4po-angular/src/assets/@theme/styles/styles.scss @@ -6,8 +6,6 @@ // loading progress bar theme @import './pace.theme'; - -@import './layout'; @import './overrides'; @import './variables'; @@ -23,8 +21,6 @@ body { // framework global styles @include nb-theme-global(); - @include ngx-layout(); - @include nb-overrides(); }; /* You can add global styles to this file, and also import other style files */ diff --git a/security-c4po-angular/src/assets/@theme/theme.module.ts b/security-c4po-angular/src/assets/@theme/theme.module.ts index 3c1779a..c0c7960 100644 --- a/security-c4po-angular/src/assets/@theme/theme.module.ts +++ b/security-c4po-angular/src/assets/@theme/theme.module.ts @@ -47,7 +47,7 @@ export class ThemeModule { providers: [ ...NbThemeModule.forRoot( { - name: 'corporate', + name: DARK_THEME.name, }, [CORPORATE_THEME, DARK_THEME], ).providers, diff --git a/security-c4po-angular/src/assets/i18n/de-DE.json b/security-c4po-angular/src/assets/i18n/de-DE.json index 1143d9b..65a9cb8 100644 --- a/security-c4po-angular/src/assets/i18n/de-DE.json +++ b/security-c4po-angular/src/assets/i18n/de-DE.json @@ -22,5 +22,17 @@ "title": "Einloggen", "failed": "Benutzername oder Passwort falsch", "unauthorized": "Benutzer nicht gefunden. Bitte registrieren und erneut versuchen" + }, + "project": { + "overview": { + "add.project": "Projekt hinzufügen", + "no.projects": "Keine Projekte verfügbar" + }, + "popup": { + "not.found": "Keine Projekte gefunden" + }, + "client": "Klient", + "tester": "Tester", + "createdAt": "Erstellt am" } } diff --git a/security-c4po-angular/src/assets/i18n/en-US.json b/security-c4po-angular/src/assets/i18n/en-US.json index fa94e75..9e7d8a5 100644 --- a/security-c4po-angular/src/assets/i18n/en-US.json +++ b/security-c4po-angular/src/assets/i18n/en-US.json @@ -11,7 +11,7 @@ "warning": "!", "info": "", "error.position": { - "permissionDenied": "Permission Denied", + "permissionDenied": "Permission denied", "timeout": "Timeout" }, "login": { @@ -22,5 +22,17 @@ "title": "Login", "failed": "Wrong username or password", "unauthorized": "User not found. Please register and try again" + }, + "project": { + "overview": { + "add.project": "Add project", + "no.projects": "No projects available" + }, + "popup": { + "not.found": "No projects found" + }, + "client": "Client", + "tester": "Tester", + "createdAt": "Created at" } } diff --git a/security-c4po-angular/src/assets/images/dashboard/project_default_logo.png b/security-c4po-angular/src/assets/images/dashboard/project_default_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b170a9aca8aae8d4c488c45818c509f33d5ae519 GIT binary patch literal 9116 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 b>Hh}+W8HV!R^@UI00000NkvXXu0mjf^nlK1 literal 0 HcmV?d00001 diff --git a/security-c4po-angular/src/assets/images/favicons/corporate_favicon.ico b/security-c4po-angular/src/assets/images/favicons/corporate_favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a2309ee6946a3a39846e43df6d6584a4f55b8d18 GIT binary patch literal 270398 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 DB>)(@ diff --git a/security-c4po-angular/src/index.html b/security-c4po-angular/src/index.html index 63035e5..d4d461d 100644 --- a/security-c4po-angular/src/index.html +++ b/security-c4po-angular/src/index.html @@ -5,7 +5,7 @@ SecurityC4POAngular - + diff --git a/security-c4po-angular/src/shared/guards/auth-guard.service.ts b/security-c4po-angular/src/shared/guards/auth-guard.service.ts index c1f61b4..5feb4b5 100644 --- a/security-c4po-angular/src/shared/guards/auth-guard.service.ts +++ b/security-c4po-angular/src/shared/guards/auth-guard.service.ts @@ -1,12 +1,8 @@ import {Injectable} from '@angular/core'; import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; import {Store} from '@ngxs/store'; -import {Observable, of} from 'rxjs'; -import {SessionState} from '../stores/session-state/session-state'; -import {catchError, map} from 'rxjs/operators'; import {KeycloakAuthGuard, KeycloakService} from 'keycloak-angular'; import {UpdateIsAuthenticated, UpdateUser} from '../stores/session-state/session-state.actions'; -import {User} from '../models/user.model'; @Injectable({ providedIn: 'root' diff --git a/security-c4po-angular/src/shared/guards/login-guard.service.spec.ts b/security-c4po-angular/src/shared/guards/login-guard.service.spec.ts deleted file mode 100644 index 1f2767d..0000000 --- a/security-c4po-angular/src/shared/guards/login-guard.service.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { LoginGuardService } from './login-guard.service'; -import {RouterTestingModule} from '@angular/router/testing'; -import {HttpClientTestingModule} from '@angular/common/http/testing'; -import {NgxsModule} from '@ngxs/store'; -import {SessionState} from '../stores/session-state/session-state'; -import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; -import {HttpLoaderFactory} from '../../app/common-app.module'; -import {HttpClient} from '@angular/common/http'; -import {KeycloakService} from 'keycloak-angular'; - -describe('LoginGuardService', () => { - let service: LoginGuardService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - RouterTestingModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [HttpClient] - } - }), - NgxsModule.forRoot([SessionState]) - ], - providers: [ - KeycloakService - ] - }); - service = TestBed.inject(LoginGuardService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/security-c4po-angular/src/shared/guards/login-guard.service.ts b/security-c4po-angular/src/shared/guards/login-guard.service.ts deleted file mode 100644 index 831d852..0000000 --- a/security-c4po-angular/src/shared/guards/login-guard.service.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Injectable } from '@angular/core'; -import {Store} from '@ngxs/store'; -import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; -import {Observable, of} from 'rxjs'; -import {catchError, map, tap} from 'rxjs/operators'; -import {SessionState} from '../stores/session-state/session-state'; -import {KeycloakAuthGuard, KeycloakService} from 'keycloak-angular'; -import {UpdateIsAuthenticated, UpdateUser} from '../stores/session-state/session-state.actions'; - -@Injectable({ - providedIn: 'root' -}) -export class LoginGuardService extends KeycloakAuthGuard implements CanActivate { - constructor( - public readonly router: Router, - protected keycloakAngular: KeycloakService, - private readonly store: Store) { - super(router, keycloakAngular); - } - - isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { - return new Promise((resolve, reject) => { - if (!this.authenticated) { - this.keycloakAngular.login() - .catch(e => console.error(e)); - return reject(false); - } - - const requiredRoles: string[] = route.data.roles; - if (!requiredRoles || requiredRoles.length === 0) { - this.store.dispatch(new UpdateIsAuthenticated(true)); - this.store.dispatch(new UpdateUser(route.data.user, true)); - return resolve(true); - } else { - if (!this.roles || this.roles.length === 0) { - this.store.dispatch(new UpdateIsAuthenticated(false)); - this.store.dispatch(new UpdateUser(null, true)); - resolve(false); - } - resolve(requiredRoles.every(role => this.roles.indexOf(role) > -1)); - } - }); - } - -/* canActivate(routeSnapshot: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.isAuthenticated().pipe( - tap((canAccess: boolean) => { - if (canAccess) { - this.router.navigate(['']).then(); - } - }), - // return true if login should be loaded (=> invert) - map((canAccess: boolean) => !canAccess) - ); - } - - /!** - * @return state of authentication - *!/ - private isAuthenticated(): Observable { - // ToDo: Should check from Authentication Provider - return of(this.store.selectSnapshot(SessionState.isAuthenticated)) - .pipe( - map((isLoggedIn: boolean) => { - return isLoggedIn; - }), - catchError(() => { - return of(false); - }) - ); - }*/ -} - diff --git a/security-c4po-angular/src/shared/models/custom-pipe.mode.ts b/security-c4po-angular/src/shared/models/custom-pipe.mode.ts new file mode 100644 index 0000000..4c0a8a3 --- /dev/null +++ b/security-c4po-angular/src/shared/models/custom-pipe.mode.ts @@ -0,0 +1,10 @@ +export enum CustomPipe { + DATE_FMT_EN = 'MM/dd/yyyy', + DATE_FMT_DE = 'dd.MM.yyyy', + DATE_TIME_FMT_EN = 'MM/dd/yyyy hh:mm a', + DATE_TIME_FMT_DE = 'dd.MM.yyyy HH:mm', + TIME_FMT_EN = 'hh:mm a', + TIME_FMT_DE = 'HH:mm', + PERCENT_FMT = '3.1-2', + TIME_FORMAT = 'shortTime' +} diff --git a/security-c4po-angular/src/shared/models/number-and-date-time-format.model.ts b/security-c4po-angular/src/shared/models/number-and-date-time-format.model.ts new file mode 100644 index 0000000..5fbc067 --- /dev/null +++ b/security-c4po-angular/src/shared/models/number-and-date-time-format.model.ts @@ -0,0 +1,4 @@ +export enum NumberAndDateFormatSystem { + ENGLISH = 'en-US', + GERMAN = 'de-DE' +} diff --git a/security-c4po-angular/src/shared/models/project.model.ts b/security-c4po-angular/src/shared/models/project.model.ts index 6f25567..971f868 100644 --- a/security-c4po-angular/src/shared/models/project.model.ts +++ b/security-c4po-angular/src/shared/models/project.model.ts @@ -1,5 +1,3 @@ -import { v4 as UUID } from 'uuid'; - export class Project { id: string; client: string; diff --git a/security-c4po-angular/src/shared/pipes/date-time-format.pipe.spec.ts b/security-c4po-angular/src/shared/pipes/date-time-format.pipe.spec.ts new file mode 100644 index 0000000..40ce517 --- /dev/null +++ b/security-c4po-angular/src/shared/pipes/date-time-format.pipe.spec.ts @@ -0,0 +1,128 @@ +import {DateTimeFormatPipe} from './date-time-format.pipe'; +import {formatDate, registerLocaleData} from '@angular/common'; +import localeDe from '@angular/common/locales/de'; +import {CustomPipe} from 'src/shared/models/custom-pipe.mode'; +import {User} from '@shared/models/user.model'; +import {UpdateUserSettings} from '@shared/stores/session-state/session-state.actions'; +import {NumberAndDateFormatSystem} from '@shared/models/number-and-date-time-format.model'; +import {SESSION_STATE_NAME, SessionState, SessionStateModel} from '@shared/stores/session-state/session-state'; +import {NgxsModule, Store} from '@ngxs/store'; +import {UserService} from '@shared/services/user.service'; +import {inject, TestBed} from '@angular/core/testing'; +import {HttpClient} from '@angular/common/http'; +import {HttpLoaderFactory} from '../../app/common-app.module'; +import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; + +const DESIRED_STORE_STATE_SESSION: SessionStateModel = { + userAccount: { + ...new User('ttt', 'test', 'user', 'default.user@test.de', 'en-US'), + id: '11c47c56-3bcd-45f1-a05b-c197dbd33110' + }, + isAuthenticated: true +}; + +describe('DateTimeFormatPipe', () => { + let pipe: DateTimeFormatPipe; + let store: Store; + // tslint:disable-next-line:prefer-const + let dateAndNumberFormat: NumberAndDateFormatSystem; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }), + NgxsModule.forRoot([SessionState]), + ], + declarations: [ + DateTimeFormatPipe, + ], + providers: [ + {provide: UserService}, + ] + }).compileComponents(); + }); + + beforeEach(inject([Store], (inStore: Store) => { + store = inStore; + store.reset({ + ...store.snapshot(), + [SESSION_STATE_NAME]: DESIRED_STORE_STATE_SESSION + }); + pipe = new DateTimeFormatPipe(inStore); + }) + ); + + it('should init', () => { + expect(pipe).toBeTruthy(); + }); + + it('return "-" if value is null', () => { + dateAndNumberFormat = NumberAndDateFormatSystem.ENGLISH; + const value: string = null; + expect(pipe.transform(value)).toEqual('-'); + }); +// ToDo: Enable tests after angular version upgrade +/* + it('return english date time format if NumberAndDateFormatSystem is undefined', () => { + dateAndNumberFormat = undefined; + const value = '2019-11-24T14:34:16.802Z'; + const oldFormatDate = formatDate; + const formatSpy = jest.spyOn(require('@angular/common'), 'formatDate') + .mockReturnValue((val, format, locale, timezone) => { + expect(val).toBe(value); + expect(format).toBe(CustomPipe.DATE_TIME_FMT_EN); + expect(locale).toBe('en-US'); + expect(timezone).toBeUndefined(); + return oldFormatDate(val, format, locale, timezone); + }); + pipe.transform(value); + // Does unfortunately not work here: expect(formatSpy).toHaveBeenCalledWith(value, CustomPipe.DATE_FMT_EN, 'en-us'); + expect(formatSpy).toHaveBeenCalledTimes(1); + }); + + it('return english date time format if NumberAndDateFormatSystem is english', () => { + dateAndNumberFormat = NumberAndDateFormatSystem.ENGLISH; + const value = '2019-11-24T14:34:16.802Z'; + const oldFormatDate = formatDate; + const formatSpy = jest.spyOn(require('@angular/common'), 'formatDate') + .mockReturnValue((val, format, locale, timezone) => { + expect(val).toBe(value); + expect(format).toBe(CustomPipe.DATE_TIME_FMT_EN); + expect(locale).toBe('en-US'); + expect(timezone).toBeUndefined(); + return oldFormatDate(val, format, locale, timezone); + }); + pipe.transform(value); + // Does unfortunately not work here: expect(formatSpy).toHaveBeenCalledWith(value, CustomPipe.DATE_FMT_EN, 'en-us'); + expect(formatSpy).toHaveBeenCalledTimes(1); + }); + + it('return german date time format if NumberAndDateFormatSystem is german', () => { + registerLocaleData(localeDe, 'de-DE'); + dateAndNumberFormat = NumberAndDateFormatSystem.GERMAN; + const userAccountUpdate = new User('test', 'test', 'test', 'test@test.de', 'de-DE'); + store.dispatch(new UpdateUserSettings(userAccountUpdate)); + const value = '2019-11-24T14:34:16.802Z'; + const oldFormatDate = formatDate; + const formatSpy = jest.spyOn(require('@angular/common'), 'formatDate') + .mockReturnValue((val, format, locale, timezone) => { + expect(val).toBe(value); + expect(format).toBe(CustomPipe.DATE_TIME_FMT_DE); + expect(locale).toBe('de-DE'); + expect(timezone).toBeUndefined(); + return oldFormatDate(val, format, locale, timezone); + }); + expect(pipe.transform(value).endsWith(' Uhr')).toBeTruthy(); + // Does unfortunately not work here: expect(formatSpy).toHaveBeenCalledWith(value, CustomPipe.DATE_FMT_EN, 'en-us'); + expect(formatSpy).toHaveBeenCalledTimes(1); + }); +*/ +}); diff --git a/security-c4po-angular/src/shared/pipes/date-time-format.pipe.ts b/security-c4po-angular/src/shared/pipes/date-time-format.pipe.ts new file mode 100644 index 0000000..3355552 --- /dev/null +++ b/security-c4po-angular/src/shared/pipes/date-time-format.pipe.ts @@ -0,0 +1,39 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {formatDate} from '@angular/common'; +import {Store} from '@ngxs/store'; +import {SessionState} from '@shared/stores/session-state/session-state'; +import {CustomPipe} from '@shared/models/custom-pipe.mode'; + +@Pipe({ + name: 'dateTimeFormat' +}) +export class DateTimeFormatPipe implements PipeTransform { + + constructor(private store: Store) { + } + + /** + * Transforms the value to the appropriate date and time format according to the selected number and date format + * @param value of type any + * @return formatted value as string + */ + transform(value: any): string { + if (!value) { + return '-'; + } + + const localeDateAndNumberFormat = this.store.selectSnapshot(SessionState.userAccount) ? + // @ts-ignore + this.store.selectSnapshot(SessionState.userAccount.interfaceLang) : 'en-US'; + + if (!localeDateAndNumberFormat) { + return formatDate(value, CustomPipe.DATE_TIME_FMT_EN, 'en-US'); + } + if (localeDateAndNumberFormat === 'de-DE') { + return formatDate(value, CustomPipe.DATE_TIME_FMT_DE, localeDateAndNumberFormat) + ' Uhr'; + } + // @ts-ignore + return formatDate(value, CustomPipe.DATE_TIME_FMT_EN, localeDateAndNumberFormat); + } + +} diff --git a/security-c4po-angular/src/shared/services/project.service.mock.ts b/security-c4po-angular/src/shared/services/project.service.mock.ts new file mode 100644 index 0000000..150d583 --- /dev/null +++ b/security-c4po-angular/src/shared/services/project.service.mock.ts @@ -0,0 +1,14 @@ +import {ProjectService} from '@shared/services/project.service'; +import {HttpClient} from '@angular/common/http'; +import {Observable, of} from 'rxjs'; +import {Project} from '@shared/models/project.model'; + + +export class ProjectServiceMock implements Required { + + private http: HttpClient; + + getProjects(): Observable { + return of([]); + } +} diff --git a/security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.html b/security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.html new file mode 100644 index 0000000..12b4729 --- /dev/null +++ b/security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.scss b/security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.spec.ts b/security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.spec.ts new file mode 100644 index 0000000..d7b70fb --- /dev/null +++ b/security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.spec.ts @@ -0,0 +1,31 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {LoadingSpinnerComponent} from './loading-spinner.component'; +import {NbSpinnerModule} from '@nebular/theme'; + +describe('LoadingSpinnerComponent', () => { + let component: LoadingSpinnerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + LoadingSpinnerComponent + ], + imports: [ + NbSpinnerModule + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LoadingSpinnerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.ts b/security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.ts new file mode 100644 index 0000000..949d45a --- /dev/null +++ b/security-c4po-angular/src/shared/widgets/loading-spinner/loading-spinner.component.ts @@ -0,0 +1,26 @@ +import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Observable, of} from 'rxjs'; +import {untilDestroyed} from 'ngx-take-until-destroy'; + +@Component({ + selector: 'app-loading-spinner', + templateUrl: './loading-spinner.component.html', + styleUrls: ['./loading-spinner.component.scss'] +}) +export class LoadingSpinnerComponent implements OnInit, OnDestroy { + + @Input() isLoading$: Observable = of(false); + loading: boolean; + + ngOnInit(): void { + this.loading = false; + this.isLoading$ + .pipe(untilDestroyed(this)) + .subscribe((value: boolean): void => { + this.loading = value; + }); + } + + ngOnDestroy(): void { + } +} diff --git a/security-c4po-angular/tsconfig.app.json b/security-c4po-angular/tsconfig.app.json index 82d91dc..ba8f5b0 100644 --- a/security-c4po-angular/tsconfig.app.json +++ b/security-c4po-angular/tsconfig.app.json @@ -5,6 +5,9 @@ "outDir": "./out-tsc/app", "types": [] }, + "angularCompilerOptions": { + "enableIvy": false + }, "files": [ "src/main.ts", "src/polyfills.ts" diff --git a/security-c4po-angular/tsconfig.json b/security-c4po-angular/tsconfig.json index c172f60..bf60e0f 100644 --- a/security-c4po-angular/tsconfig.json +++ b/security-c4po-angular/tsconfig.json @@ -18,7 +18,7 @@ "dom" ], "paths": { - "@shared/*": ["./src/app/shared/*"], + "@shared/*": ["./src/shared/*"], "@assets/*": ["./src/assets/*"] } } diff --git a/security-c4po-angular/tsconfig.spec.json b/security-c4po-angular/tsconfig.spec.json index 58c6bb3..2b6775c 100644 --- a/security-c4po-angular/tsconfig.spec.json +++ b/security-c4po-angular/tsconfig.spec.json @@ -7,7 +7,7 @@ ], "module": "commonjs", "emitDecoratorMetadata": true, - "allowJs": true + "allowJs": true, }, "files": [ "src/polyfills.ts" diff --git a/security-c4po-api/build.gradle.kts b/security-c4po-api/build.gradle.kts index 828fab9..f200e78 100644 --- a/security-c4po-api/build.gradle.kts +++ b/security-c4po-api/build.gradle.kts @@ -74,7 +74,6 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") - implementation("com.auth0:java-jwt:3.18.1") implementation("org.modelmapper:modelmapper:2.3.2") api("org.springframework.security:spring-security-jwt:1.1.1.RELEASE") diff --git a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc index 7f40413..08bfb63 100644 --- a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc +++ b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc @@ -29,15 +29,15 @@ To get projects, call the GET request /projects ==== Request example -#include::{snippets}/getProjects/com.securityc4po.api.http-request.adoc[] +include::{snippets}/getProjects/http-request.adoc[] ==== Response example -#include::{snippets}/getProjects/com.securityc4po.api.http-response.adoc[] +include::{snippets}/getProjects/http-response.adoc[] ==== Response structure -#include::{snippets}/getProjects/response-fields.adoc[] +include::{snippets}/getProjects/response-fields.adoc[] == Change History diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/BaseEntity.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/BaseEntity.kt index c18583d..8bc5572 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/BaseEntity.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/BaseEntity.kt @@ -1,6 +1,19 @@ package com.securityc4po.api +import com.securityc4po.api.configuration.MESSAGE_NOT_INITIALIZED_REDUNDANT_NULLCHECK +import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR +import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import org.springframework.data.annotation.Id + +@SuppressFBWarnings( + NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR, + RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE, + MESSAGE_NOT_INITIALIZED_REDUNDANT_NULLCHECK +) abstract class BaseEntity( var data: T ) { + @Id + lateinit var id: String } diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/Appuser.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/Appuser.kt index 0db770d..b292d4e 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/Appuser.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/Appuser.kt @@ -5,11 +5,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.userdetails.UserDetails -class Appuser internal constructor( - val sub: String, - val extractedUsername: String, - val token: String -) : UserDetails { +class Appuser internal constructor() : UserDetails { override fun getAuthorities(): Collection { return listOf("user").stream().map { diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/AppuserJwtAuthConverter.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/AppuserJwtAuthConverter.kt index d1616b5..e313477 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/AppuserJwtAuthConverter.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/AppuserJwtAuthConverter.kt @@ -9,17 +9,20 @@ import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.oauth2.jwt.Jwt import reactor.core.publisher.Mono -import reactor.kotlin.core.publisher.toMono import java.util.stream.Collectors -/** JWT converter that takes the roles from 'groups' claim of JWT token. */ -class AppuserJwtAuthConverter : Converter> { +class AppuserJwtAuthConverter(private val appuserDetailsService: UserAccountDetailsService) : + Converter> { override fun convert(jwt: Jwt): Mono { val authorities = extractAuthorities(jwt) - val sub = extractSub(jwt) + // val sub = extractSub(jwt) val username = extractUserName(jwt) - return UsernamePasswordAuthenticationToken(Appuser(sub, username, jwt.tokenValue!!), "n/a", authorities).toMono() + return appuserDetailsService + .findByUsername(username) + .map { user -> + UsernamePasswordAuthenticationToken(user, "n/a", authorities); + } } private fun extractSub(jwt: Jwt): String { @@ -51,7 +54,7 @@ class AppuserJwtAuthConverter : Converter val scopes = jwt.getClaims().get(GROUPS_CLAIM).toString() val roleStringValue = mapper.readTree(scopes).get("roles").toString() val roles = mapper.readValue>(roleStringValue) - if (!roles.isEmpty()){ + if (!roles.isEmpty()) { return roles } return emptyList() diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/UserAccountDetailsService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/UserAccountDetailsService.kt new file mode 100644 index 0000000..56c485c --- /dev/null +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/UserAccountDetailsService.kt @@ -0,0 +1,14 @@ +package com.securityc4po.api.configuration.security + +import org.springframework.security.core.userdetails.ReactiveUserDetailsService +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.stereotype.Service +import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.toMono + +@Service +class UserAccountDetailsService : ReactiveUserDetailsService { + override fun findByUsername(username: String): Mono { + return Appuser().toMono() + } +} \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/WebSecurityConfiguration.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/WebSecurityConfiguration.kt index 27cb711..0ba7402 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/WebSecurityConfiguration.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/security/WebSecurityConfiguration.kt @@ -1,8 +1,9 @@ package com.securityc4po.api.configuration.security -import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration +import org.springframework.boot.autoconfigure.EnableAutoConfiguration import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Lazy +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity @@ -12,10 +13,12 @@ import org.springframework.web.cors.CorsConfiguration @EnableWebFluxSecurity @EnableReactiveMethodSecurity -class WebSecurityConfiguration { +@Configuration +@ComponentScan +class WebSecurityConfiguration(private val userAccountDetailsService: UserAccountDetailsService) { @Bean - fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + fun setSecurityWebFilterChains(http: ServerHttpSecurity): SecurityWebFilterChain { http.cors().configurationSource { CorsConfiguration().apply { this.applyPermitDefaultValues() @@ -43,6 +46,6 @@ class WebSecurityConfiguration { @Bean fun appuserJwtAuthenticationConverter(): AppuserJwtAuthConverter { - return AppuserJwtAuthConverter() + return AppuserJwtAuthConverter(userAccountDetailsService) } } diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectEntity.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectEntity.kt index 9e6281b..daf88fa 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectEntity.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectEntity.kt @@ -1,6 +1,9 @@ package com.securityc4po.api.project import com.securityc4po.api.BaseEntity +import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION +import com.securityc4po.api.configuration.MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import org.springframework.data.mongodb.core.mapping.Document @Document(collection = "projects") @@ -19,6 +22,7 @@ fun ProjectEntity.toProject() : Project { ) } +@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION) fun List.toProjects(): List { return this.map { it.toProject() diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt index 458434e..98163c2 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt @@ -1,10 +1,14 @@ package com.securityc4po.api.project +import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION +import com.securityc4po.api.configuration.MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION import com.securityc4po.api.extensions.getLoggerFor +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import org.springframework.stereotype.Service import reactor.core.publisher.Mono @Service +@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION) class ProjectService(private val projectRepository: ProjectRepository) { var logger = getLoggerFor() diff --git a/security-c4po-api/src/main/resources/application-COMPOSE.properties b/security-c4po-api/src/main/resources/application-COMPOSE.properties index 11fe6cf..e4888d6 100644 --- a/security-c4po-api/src/main/resources/application-COMPOSE.properties +++ b/security-c4po-api/src/main/resources/application-COMPOSE.properties @@ -1,7 +1,8 @@ ## IdentityProvider (Keycloak) ## -# spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local -# keycloakhost=localhost -# keycloak.client.url=http://localhost:8888/ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local +keycloakhost=localhost +keycloak.client.url=http://localhost:8888 +keycloak.client.realm.path=auth/realms/c4po_realm_local/ ## Database (MONGODB) Config ## spring.data.mongodb.host=c4po-db diff --git a/security-c4po-api/src/main/resources/application-DEV.properties b/security-c4po-api/src/main/resources/application-DEV.properties index f3fb2e6..9c88551 100644 --- a/security-c4po-api/src/main/resources/application-DEV.properties +++ b/security-c4po-api/src/main/resources/application-DEV.properties @@ -1,7 +1,7 @@ ## IdentityProvider (Keycloak) ## -# spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local -# keycloakhost=localhost -# keycloak.client.url=http://localhost:8888/ +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local +keycloakhost=localhost +keycloak.client.url=http://localhost:8888/ ## Database (MONGODB) Config ## spring.data.mongodb.host=localhost diff --git a/security-c4po-api/src/main/resources/application-TEST.properties b/security-c4po-api/src/main/resources/application-TEST.properties new file mode 100644 index 0000000..d8e6f76 --- /dev/null +++ b/security-c4po-api/src/main/resources/application-TEST.properties @@ -0,0 +1,9 @@ +## IdentityProvider (Keycloak) ## +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9999/auth/realms/c4po_realm_local +keycloakhost=localhost +keycloak.client.url=http://localhost:9999 +keycloak.client.realm.path=auth/realms/c4po_realm_local/ + +## Database (MONGODB) Config ## +spring.data.mongodb.host=localhost +spring.data.mongodb.port=27021 \ No newline at end of file diff --git a/security-c4po-api/src/main/resources/application.properties b/security-c4po-api/src/main/resources/application.properties index 327d23c..6933b14 100644 --- a/security-c4po-api/src/main/resources/application.properties +++ b/security-c4po-api/src/main/resources/application.properties @@ -18,6 +18,5 @@ spring.data.mongodb.auto-index-creation=true ## IdentityProvider (Keycloak) ## spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local keycloakhost=localhost -keycloak.client.url=http://localhost:8888/ -# keycloak.client.realm.path=auth/realms/c4po_realm_local/ -idp.jwt.claim.name.user=username \ No newline at end of file +keycloak.client.url=http://localhost:8888 +keycloak.client.realm.path=auth/realms/c4po_realm_local/ \ No newline at end of file diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/BaseContainerizedTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/BaseContainerizedTest.kt index c8f98c4..f1b69c2 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/BaseContainerizedTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/BaseContainerizedTest.kt @@ -11,6 +11,7 @@ import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders import org.springframework.http.MediaType +import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.TestPropertySource import org.springframework.util.LinkedMultiValueMap import org.springframework.web.client.RestTemplate @@ -24,7 +25,7 @@ import java.nio.file.Paths @TestInstance(TestInstance.Lifecycle.PER_CLASS) @AutoConfigureWireMock(port = 0) @TestPropertySource(properties = [ - "spring.data.mongodb.port=10002", + "spring.data.mongodb.port=27017", "spring.data.mongodb.authentication-database=admin", "spring.data.mongodb.password=test", "spring.data.mongodb.username=testuser", @@ -46,12 +47,12 @@ abstract class BaseContainerizedTest { }.withFileFromPath("insert-mongodb-user.js", Paths.get(MountableFile.forClasspathResource("insert-mongodb-user.js", 700).resolvedPath)) ).apply { withCreateContainerCmdModifier { - it.hostConfig?.withPortBindings(PortBinding(Ports.Binding.bindPort(10002), ExposedPort(27017))) + it.hostConfig?.withPortBindings(PortBinding(Ports.Binding.bindPort(27017), ExposedPort(27017))) } start() } - val keycloakContainer = KGenericContainerFromImage(DockerImageName.parse("jboss/keycloak:6.0.1")).apply { + val keycloakContainer = KGenericContainerFromImage(DockerImageName.parse("jboss/keycloak:11.0.3")).apply { withEnv("KEYCLOAK_USER", "admin") withEnv("KEYCLOAK_PASSWORD", "admin") withEnv("KEYCLOAK_IMPORT", "/tmp/realm.json") @@ -59,9 +60,7 @@ abstract class BaseContainerizedTest { withCreateContainerCmdModifier { it.hostConfig?.withPortBindings(PortBinding(Ports.Binding.bindPort(8888), ExposedPort(8080))) } - withCopyFileToContainer(MountableFile.forClasspathResource("outdated_realm-export.json", 700), "/tmp/realm.json") - withCopyFileToContainer(MountableFile.forClasspathResource("create-keycloak-user.sh", 700), - "/opt/jboss/create-keycloak-user.sh") + withCopyFileToContainer(MountableFile.forClasspathResource("realm-export.json", 700), "/tmp/realm.json") start() println("== Inserting users must wait until Keycloak is started completely ==") execInContainer("sh", "/opt/jboss/create-keycloak-user.sh") @@ -80,10 +79,10 @@ abstract class BaseContainerizedTest { headers.contentType = MediaType.APPLICATION_FORM_URLENCODED val map = LinkedMultiValueMap() - map.add("grant_type", "password") map.add("client_id", clientId) map.add("username", username) map.add("password", password) + map.add("grant_type", "password") map.add("client_secret", "secret") val responseString = restTemplate.postForObject("$keycloakHost/auth/realms/$realm/protocol/openid-connect/token", HttpEntity(map, headers), String::class.java) diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt index a97ef60..ab79926 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt @@ -2,13 +2,15 @@ package com.securityc4po.api.project import com.github.tomakehurst.wiremock.common.Json import com.securityc4po.api.BaseDocumentationIntTest +import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR +import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired -import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Query import org.springframework.restdocs.operation.preprocess.Preprocessors @@ -16,8 +18,11 @@ import org.springframework.restdocs.payload.JsonFieldType import org.springframework.restdocs.payload.PayloadDocumentation import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation -@AutoConfigureWireMock(port = 0) -@SuppressFBWarnings(SIC_INNER_SHOULD_BE_STATIC) +@SuppressFBWarnings( + SIC_INNER_SHOULD_BE_STATIC, + NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR, + RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE +) class ProjectControllerDocumentationTest : BaseDocumentationIntTest() { @Autowired @@ -25,18 +30,21 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() { @BeforeEach fun init() { - cleanUp() + configureAdminToken() persistBasicTestScenario() } + @AfterEach + fun destroy() { + cleanUp() + } + @Nested inner class GetProjects { @Test fun getProjects() { - /* Implement after the implementation of database */ - - /*webTestClient.get().uri("/v1/projects") - .header("") + webTestClient.get().uri("/projects") + .header("Authorization", "Bearer $tokenAdmin") .exchange() .expectStatus().isOk .expectHeader().doesNotExist("") @@ -49,14 +57,14 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() { Preprocessors.prettyPrint() ), PayloadDocumentation.relaxedResponseFields( - PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING).description("The id of the requested Project"), - PayloadDocumentation.fieldWithPath("[].client").type(JsonFieldType.STRING).description("The name of the client of the requested Project"), - PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING).description("The title of the requested Project"), - PayloadDocumentation.fieldWithPath("[].createdAt").type(JsonFieldType.STRING).description("The date where the Project was created at"), - PayloadDocumentation.fieldWithPath("[].tester").type(JsonFieldType.STRING).description("The user that is used as a tester in the Project"), - PayloadDocumentation.fieldWithPath("[].logo").type(JsonFieldType.STRING).description("The sensors contained in the Project") + PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING).description("The id of the requested project"), + PayloadDocumentation.fieldWithPath("[].client").type(JsonFieldType.STRING).description("The name of the client of the requested project"), + PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING).description("The title of the requested project"), + PayloadDocumentation.fieldWithPath("[].createdAt").type(JsonFieldType.STRING).description("The date where the project was created at"), + PayloadDocumentation.fieldWithPath("[].tester").type(JsonFieldType.STRING).description("The user that is assigned as a tester in the project"), + PayloadDocumentation.fieldWithPath("[].createdBy").type(JsonFieldType.STRING).description("The id of the user that created the project") ) - ))*/ + )) } val projectOne = Project( @@ -82,30 +90,36 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() { ) } - private fun cleanUp() { - mongoTemplate.findAllAndRemove(Query(), Project::class.java) - } - private fun persistBasicTestScenario() { // setup test data val projectOne = Project( - id = "260aa538-0873-43fc-84de-3a09b008646d", - client = "", - title = "", - createdAt = "", - tester = "", - createdBy = "" + id = "4f6567a8-76fd-487b-8602-f82d0ca4d1f9", + client = "E Corp", + title = "Some Mock API (v1.0) Scanning", + createdAt = "2021-01-10T18:05:00Z", + tester = "Novatester", + createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" ) val projectTwo = Project( - id = "260aa538-0873-43fc-84de-3a09b008646d", - client = "", - title = "", - createdAt = "", - tester = "", - createdBy = "" + id = "61360a47-796b-4b3f-abf9-c46c668596c5", + client = "Allsafe", + title = "CashMyData (iOS)", + createdAt = "2021-01-10T18:05:00Z", + tester = "Elliot", + createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" ) - cleanUp() + // persist test data in database mongoTemplate.save(ProjectEntity(projectOne)) mongoTemplate.save(ProjectEntity(projectTwo)) } + + private fun configureAdminToken() { + tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local") + } + + private fun cleanUp() { + mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java) + + tokenAdmin = "n/a" + } } \ No newline at end of file diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt index e2988fb..fe56a2e 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt @@ -2,34 +2,25 @@ package com.securityc4po.api.project import com.github.tomakehurst.wiremock.common.Json import com.securityc4po.api.BaseIntTest +import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR +import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC -import com.securityc4po.api.configuration.URF_UNREAD_FIELD import edu.umd.cs.findbugs.annotations.SuppressFBWarnings -import io.netty.handler.ssl.SslContextBuilder +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.web.server.LocalServerPort -import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Query -import org.springframework.test.context.TestPropertySource import org.springframework.test.web.reactive.server.WebTestClient -import org.springframework.util.ResourceUtils -import reactor.netty.http.client.HttpClient import java.time.Duration -@AutoConfigureWireMock(port = 0) -/*@TestPropertySource( - properties = [ - "keycloak.client.url=http://localhost:${'$'}{wiremock.server.port}" - ] -)*/ @SuppressFBWarnings( SIC_INNER_SHOULD_BE_STATIC, - URF_UNREAD_FIELD, - "Unread field will become used after database implementation" + NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR, + RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE ) class ProjectControllerIntTest : BaseIntTest() { @@ -39,6 +30,7 @@ class ProjectControllerIntTest : BaseIntTest() { @Autowired lateinit var mongoTemplate: MongoTemplate + @Autowired private lateinit var webTestClient: WebTestClient @BeforeEach @@ -51,20 +43,24 @@ class ProjectControllerIntTest : BaseIntTest() { @BeforeEach fun init() { - cleanUp() configureAdminToken() persistBasicTestScenario() } + @AfterEach + fun destroy() { + cleanUp() + } + @Nested inner class GetProjects { @Test fun `requesting projects successfully`() { - webTestClient.get().uri("/v1/projects") + webTestClient.get().uri("/projects") .header("Authorization", "Bearer $tokenAdmin") .exchange() .expectStatus().isOk - .expectHeader().valueEquals("Application-Name", "security-c4po-api") + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") .expectBody().json(Json.write(getProjects())) } @@ -91,12 +87,6 @@ class ProjectControllerIntTest : BaseIntTest() { ) } - private fun cleanUp() { - mongoTemplate.findAllAndRemove(Query(), Project::class.java) - - tokenAdmin = "n/a" - } - private fun persistBasicTestScenario() { // setup test data val projectOne = Project( @@ -115,7 +105,7 @@ class ProjectControllerIntTest : BaseIntTest() { tester = "Elliot", createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" ) - cleanUp() + // persist test data in database mongoTemplate.save(ProjectEntity(projectOne)) mongoTemplate.save(ProjectEntity(projectTwo)) } @@ -123,4 +113,10 @@ class ProjectControllerIntTest : BaseIntTest() { private fun configureAdminToken() { tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local") } + + private fun cleanUp() { + mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java) + + tokenAdmin = "n/a" + } } \ No newline at end of file diff --git a/security-c4po-api/src/test/resources/create-keycloak-user.sh b/security-c4po-api/src/test/resources/create-keycloak-user.sh deleted file mode 100644 index 4ec0d95..0000000 --- a/security-c4po-api/src/test/resources/create-keycloak-user.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -cd keycloak/bin -sleep 20 -./kcadm.sh config credentials --server http://localhost:8080/auth --realm master --user admin --password admin - -USERID=$(./kcadm.sh create users -r c4po_realm_local -s username=test_admin \ --s email=Test.Admin@heros.com \ --s firstName=test \ --s lastName=admin \ --s attributes.lang="de-DE" \ --s attributes.datenumberformat="en-US" \ --o --fields id | jq '.id' | tr -d '"') - -./kcadm.sh update users/$USERID/reset-password -r c4po_realm_test -s type=password -s value=test -s temporary=false -n -./kcadm.sh add-roles --uusername test_admin --rolename c4po_admin -r c4po_realm_test -./kcadm.sh add-roles -r c4po_realm_test --uusername test_admin --cclientid realm-management --rolename create-client --rolename view-users - -USERID=$(./kcadm.sh create users -r c4po_realm_local -s username=test_user \ --s email=Test.User@heros.com \ --s firstName=test \ --s lastName=user \ --s attributes.lang="de-DE" \ --s attributes.datenumberformat="en-US" \ --o --fields id | jq '.id' | tr -d '"') - -./kcadm.sh update users/$USERID/reset-password -r c4po_realm_test -s type=password -s value=test -s temporary=false -n -./kcadm.sh add-roles --uusername test_user --rolename c4po_user -r c4po_realm_test -./kcadm.sh add-roles -r c4po_realm_test --uusername test_user --cclientid realm-management --rolename create-client --rolename view-users \ No newline at end of file diff --git a/security-c4po-api/src/test/resources/outdated_realm-export.json b/security-c4po-api/src/test/resources/outdated_realm-export.json deleted file mode 100644 index d321e8f..0000000 --- a/security-c4po-api/src/test/resources/outdated_realm-export.json +++ /dev/null @@ -1,2230 +0,0 @@ -{ - "id": "c4po_realm_local", - "realm": "c4po_realm_local", - "notBefore": 0, - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 300, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "ssoSessionIdleTimeoutRememberMe": 0, - "ssoSessionMaxLifespanRememberMe": 0, - "offlineSessionIdleTimeout": 2592000, - "offlineSessionMaxLifespanEnabled": false, - "offlineSessionMaxLifespan": 5184000, - "accessCodeLifespan": 60, - "accessCodeLifespanUserAction": 300, - "accessCodeLifespanLogin": 1800, - "actionTokenGeneratedByAdminLifespan": 43200, - "actionTokenGeneratedByUserLifespan": 300, - "enabled": true, - "sslRequired": "external", - "registrationAllowed": false, - "registrationEmailAsUsername": false, - "rememberMe": false, - "verifyEmail": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": false, - "editUsernameAllowed": false, - "bruteForceProtected": false, - "permanentLockout": false, - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "roles": { - "realm": [ - { - "id": "ffe6c698-0281-404a-8f8b-16b8b489f76f", - "name": "offline_access", - "description": "${role_offline-access}", - "composite": false, - "clientRole": false, - "containerId": "c4po_realm_local", - "attributes": {} - }, - { - "id": "1cd0f230-beec-4e5e-9a59-8553da3ee74b", - "name": "USER", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "view-clients", - "manage-identity-providers", - "manage-users", - "manage-realm", - "query-realms", - "view-realm", - "query-users", - "query-groups", - "impersonation", - "view-authorization", - "manage-authorization", - "view-users", - "create-client", - "manage-events", - "view-events", - "manage-clients", - "realm-admin", - "view-identity-providers", - "query-clients" - ] - } - }, - "clientRole": false, - "containerId": "c4po_realm_local", - "attributes": {} - }, - { - "id": "e67c470f-5fe5-4e4b-a996-0ef8e6df7585", - "name": "PROJECTLEAD", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "view-clients", - "manage-identity-providers", - "manage-users", - "manage-realm", - "query-realms", - "view-realm", - "query-users", - "query-groups", - "impersonation", - "view-authorization", - "manage-authorization", - "view-users", - "create-client", - "manage-events", - "view-events", - "manage-clients", - "realm-admin", - "view-identity-providers", - "query-clients" - ] - } - }, - "clientRole": false, - "containerId": "c4po_realm_local", - "attributes": {} - }, - { - "id": "04320a32-4608-447a-98ab-422de17b1e97", - "name": "CUSTOMERADMIN", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "view-clients", - "manage-identity-providers", - "manage-users", - "manage-realm", - "query-realms", - "view-realm", - "query-users", - "query-groups", - "impersonation", - "view-authorization", - "manage-authorization", - "view-users", - "create-client", - "manage-events", - "view-events", - "manage-clients", - "realm-admin", - "view-identity-providers", - "query-clients" - ] - } - }, - "clientRole": false, - "containerId": "c4po_realm_local", - "attributes": {} - }, - { - "id": "62b91792-977d-448b-8163-8d7bc8cd7e8b", - "name": "OWNER", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "view-clients", - "manage-identity-providers", - "manage-users", - "manage-realm", - "query-realms", - "view-realm", - "query-users", - "query-groups", - "impersonation", - "view-authorization", - "manage-authorization", - "view-users", - "create-client", - "manage-events", - "view-events", - "manage-clients", - "realm-admin", - "view-identity-providers", - "query-clients" - ] - } - }, - "clientRole": false, - "containerId": "c4po_realm_local", - "attributes": {} - }, - { - "id": "f7584ecd-24e2-41ef-89bf-245671a31163", - "name": "uma_authorization", - "description": "${role_uma_authorization}", - "composite": false, - "clientRole": false, - "containerId": "c4po_realm_local", - "attributes": {} - }, - { - "id": "80382db6-cd7f-44e4-b3c3-4a59bbabdccc", - "name": "OBSERVER", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "view-clients", - "manage-identity-providers", - "manage-users", - "manage-realm", - "query-realms", - "view-realm", - "query-users", - "query-groups", - "impersonation", - "view-authorization", - "manage-authorization", - "view-users", - "create-client", - "manage-events", - "view-events", - "manage-clients", - "realm-admin", - "view-identity-providers", - "query-clients" - ] - } - }, - "clientRole": false, - "containerId": "c4po_realm_local", - "attributes": {} - }, - { - "id": "9681689b-afc8-4252-8339-5ed97eac19b5", - "name": "COMPANYADMIN", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "view-clients", - "manage-identity-providers", - "manage-users", - "manage-realm", - "query-realms", - "view-realm", - "query-users", - "query-groups", - "impersonation", - "view-authorization", - "manage-authorization", - "view-users", - "create-client", - "manage-events", - "view-events", - "manage-clients", - "realm-admin", - "view-identity-providers", - "query-clients" - ] - } - }, - "clientRole": false, - "containerId": "c4po_realm_local", - "attributes": {} - }, - { - "id": "7222919e-386a-4c70-a5d8-1b3d3bf06f57", - "name": "cuid:11c47c56-3bcd-45f1-a05b-c197dbd32111", - "composite": false, - "clientRole": false, - "containerId": "c4po_realm_local", - "attributes": {} - } - ], - "client": { - "useraccount-service": [ - { - "id": "7a7e2bee-36c1-4b59-9623-bbe4f81d069e", - "name": "CUSTOMERADMIN", - "composite": false, - "clientRole": true, - "containerId": "0dc2a195-7b23-4e58-bf81-3b6566e4c36c", - "attributes": {} - }, - { - "id": "76591939-e3e2-4e35-b9c8-14a19f0ac50b", - "name": "uma_protection", - "composite": false, - "clientRole": true, - "containerId": "0dc2a195-7b23-4e58-bf81-3b6566e4c36c", - "attributes": {} - }, - { - "id": "b9fae233-7d6b-4a12-8951-5c20fba7e560", - "name": "OBSERVER", - "composite": false, - "clientRole": true, - "containerId": "0dc2a195-7b23-4e58-bf81-3b6566e4c36c", - "attributes": {} - }, - { - "id": "f5efb02b-c6ed-48fe-92bf-47cc8b1c783f", - "name": "USER", - "composite": false, - "clientRole": true, - "containerId": "0dc2a195-7b23-4e58-bf81-3b6566e4c36c", - "attributes": {} - }, - { - "id": "74ef1b12-b236-4dfb-a251-8f9c38fedab9", - "name": "PROJECTLEAD", - "composite": false, - "clientRole": true, - "containerId": "0dc2a195-7b23-4e58-bf81-3b6566e4c36c", - "attributes": {} - }, - { - "id": "9cae2c55-361b-4d34-b4aa-55d8877948ac", - "name": "cuid:11c47c56-3bcd-45f1-a05b-c197dbd32111", - "composite": false, - "clientRole": true, - "containerId": "0dc2a195-7b23-4e58-bf81-3b6566e4c36c", - "attributes": {} - }, - { - "id": "06be41f6-43d9-48aa-8e36-dad567e2eb7f", - "name": "COMPANYADMIN", - "description": "", - "composite": false, - "clientRole": true, - "containerId": "0dc2a195-7b23-4e58-bf81-3b6566e4c36c", - "attributes": {} - }, - { - "id": "00e583d6-dedd-43f8-b7cb-e2aefe313ac5", - "name": "OWNER", - "composite": false, - "clientRole": true, - "containerId": "0dc2a195-7b23-4e58-bf81-3b6566e4c36c", - "attributes": {} - }, - { - "id": "d65244ef-f2cc-4ca1-83cd-b80d2139cb87", - "name": "SENSORADMIN", - "composite": false, - "clientRole": true, - "containerId": "0dc2a195-7b23-4e58-bf81-3b6566e4c36c", - "attributes": {} - }, - { - "id": "fe51cb66-706f-42ea-aa95-2dc9a67b7bd9", - "name": "SENSORCLIENT", - "composite": false, - "clientRole": true, - "containerId": "0dc2a195-7b23-4e58-bf81-3b6566e4c36c", - "attributes": {} - } - ], - "realm-management": [ - { - "id": "e3ee3ecd-c0f0-4f21-8ed9-26f188182873", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-clients" - ] - } - }, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "d9b446db-f795-4e96-a7ec-a4fd5efe6fbc", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "61e592ba-4f37-4f7a-83d1-9b61dd870f74", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "407b4eee-745d-4f84-a278-18cdc99e9bc7", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "ead18270-d63c-4634-853d-9da14a060c08", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "3d8b1ae3-06b3-4699-bdb4-c18a81f1deca", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "04416ae9-7437-4107-b958-802fdc8909f5", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "a33907aa-eae0-4154-85d7-6acda98bbe79", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "ddfe71ae-8181-4cd8-9fdd-2b8f2c524101", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "2d7fb697-7c73-4854-87f2-c84974b2f73a", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "e927482a-dd4b-475a-b4f9-b7224a8fbac1", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "5c8d303a-853d-4c2d-abac-77056f424a5f", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "query-users", - "query-groups" - ] - } - }, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "00ab84fb-a0b0-4ef4-8b43-e09f5da7e350", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "bf58f8f7-e097-4dec-96cd-aab047c80c99", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "50e67c01-1b81-45c9-bda1-93205f61a4e1", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "f50a4f70-3ca9-48b4-a567-3f838d93bc4a", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "e159661b-c630-4279-a7fd-da46d2aaa734", - "name": "realm-admin", - "description": "${role_realm-admin}", - "composite": true, - "composites": { - "client": { - "realm-management": [ - "view-clients", - "manage-identity-providers", - "manage-users", - "manage-realm", - "query-realms", - "view-realm", - "query-users", - "query-groups", - "impersonation", - "view-authorization", - "manage-authorization", - "view-users", - "create-client", - "manage-events", - "view-events", - "manage-clients", - "view-identity-providers", - "query-clients" - ] - } - }, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "912fb2b9-5db4-473f-820b-5048cc85e8db", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - }, - { - "id": "4672a0f6-0522-40cc-b560-68f060f6d877", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "attributes": {} - } - ], - "security-admin-console": [], - "admin-cli": [], - "broker": [ - { - "id": "6aadda6f-299f-41e4-9954-009cee812cf8", - "name": "read-token", - "description": "${role_read-token}", - "composite": false, - "clientRole": true, - "containerId": "934c22be-97c3-44b3-a5c0-2f1bdc93e540", - "attributes": {} - } - ], - "account": [ - { - "id": "b2a111c4-4574-4f09-92ea-bccf7fe7400f", - "name": "view-profile", - "description": "${role_view-profile}", - "composite": false, - "clientRole": true, - "containerId": "5f58a3c9-fb96-4565-aa97-24001d045829", - "attributes": {} - }, - { - "id": "d441f7b3-d8a8-4644-bde4-e99938520c51", - "name": "manage-account", - "description": "${role_manage-account}", - "composite": true, - "composites": { - "client": { - "account": [ - "manage-account-links" - ] - } - }, - "clientRole": true, - "containerId": "5f58a3c9-fb96-4565-aa97-24001d045829", - "attributes": {} - }, - { - "id": "68d365a0-df3f-4f12-ba1a-739acceec1e0", - "name": "manage-account-links", - "description": "${role_manage-account-links}", - "composite": false, - "clientRole": true, - "containerId": "5f58a3c9-fb96-4565-aa97-24001d045829", - "attributes": {} - } - ] - } - }, - "groups": [], - "defaultRoles": [ - "uma_authorization", - "offline_access" - ], - "requiredCredentials": [ - "password" - ], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpSupportedApplications": [ - "FreeOTP", - "Google Authenticator" - ], - "scopeMappings": [ - { - "client": "useraccount-service", - "roles": [ - "COMPANYADMIN", - "OWNER", - "cuid:11c47c56-3bcd-45f1-a05b-c197dbd32111", - "OBSERVER", - "CUSTOMERADMIN", - "PROJECTLEAD", - "USER", - "SENSORADMIN", - "SENSORCLIENT" - ] - }, - { - "clientScope": "offline_access", - "roles": [ - "offline_access" - ] - } - ], - "clientScopeMappings": { - "realm-management": [ - { - "client": "useraccount-service", - "roles": [ - "view-identity-providers", - "view-realm", - "manage-identity-providers", - "impersonation", - "realm-admin", - "create-client", - "manage-users", - "query-realms", - "view-authorization", - "query-clients", - "query-users", - "manage-events", - "manage-realm", - "view-events", - "view-users", - "view-clients", - "manage-authorization", - "manage-clients", - "query-groups" - ] - } - ], - "account": [ - { - "client": "useraccount-service", - "roles": [ - "view-profile" - ] - } - ] - }, - "clients": [ - { - "id": "0dc2a195-7b23-4e58-bf81-3b6566e4c36c", - "clientId": "useraccount-service", - "surrogateAuthRequired": false, - "enabled": true, - "clientAuthenticatorType": "client-secret", - "secret": "secret", - "redirectUris": [ - "http://awstslx070:4200/*", - "http://localhost:4200/*", - "http://awstslx071:4200/*" - ], - "webOrigins": [ - "*" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": true, - "authorizationServicesEnabled": true, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.assertion.signature": "false", - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "saml.encrypt": "false", - "saml.server.signature": "false", - "saml.server.signature.keyinfo.ext": "false", - "exclude.session.state.from.auth.response": "false", - "saml_force_name_id_format": "false", - "saml.client.signature": "false", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": -1, - "protocolMappers": [ - { - "id": "e467088f-3ad5-439e-a507-6213ec96dc7e", - "name": "Client IP Address", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientAddress", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientAddress", - "jsonType.label": "String" - } - }, - { - "id": "f593e14f-701b-4318-9c45-06b46596df9b", - "name": "Client ID", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientId", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientId", - "jsonType.label": "String" - } - }, - { - "id": "fb2f5eff-a705-4069-8578-57419ed6ea79", - "name": "Cognito Groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "multivalued": "true", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "cognito:groups", - "jsonType.label": "String" - } - }, - { - "id": "73eaf490-b6d8-4f6b-a9a0-1823c95c129d", - "name": "Client Host", - "protocol": "openid-connect", - "protocolMapper": "oidc-usersessionmodel-note-mapper", - "consentRequired": false, - "config": { - "user.session.note": "clientHost", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "clientHost", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ], - "authorizationSettings": { - "allowRemoteResourceManagement": true, - "policyEnforcementMode": "ENFORCING", - "resources": [ - { - "name": "Default Resource", - "type": "urn:useraccount-service:resources:default", - "ownerManagedAccess": false, - "attributes": {}, - "_id": "b6b38f1d-9b2f-45b4-903b-75e4caa04822", - "uris": [ - "/*" - ] - } - ], - "policies": [ - { - "id": "028c93ef-a91a-4cb4-9658-e9df81a2c7d1", - "name": "Default Policy", - "description": "A policy that grants access only for users within this realm", - "type": "js", - "logic": "POSITIVE", - "decisionStrategy": "AFFIRMATIVE", - "config": { - "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" - } - }, - { - "id": "b10ccbd7-080f-46d8-b064-1082bcbc8025", - "name": "Default Permission", - "description": "A permission that applies to the default resource type", - "type": "resource", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "config": { - "defaultResourceType": "urn:useraccount-service:resources:default", - "applyPolicies": "[\"Default Policy\"]" - } - } - ], - "scopes": [] - } - }, - { - "id": "20111584-1444-43f4-a8bb-824139f991ea", - "clientId": "security-admin-console", - "name": "${client_security-admin-console}", - "baseUrl": "/auth/admin/c4po_realm_local/console/index.html", - "surrogateAuthRequired": false, - "enabled": true, - "clientAuthenticatorType": "client-secret", - "secret": "secret", - "redirectUris": [ - "/auth/admin/c4po_realm_local/console/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "4d4a3b4a-866a-48fd-9ed7-cf86eed3ec93", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "15a976bb-49f0-4abc-a2f4-54694144aa42", - "clientId": "admin-cli", - "name": "${client_admin-cli}", - "surrogateAuthRequired": false, - "enabled": true, - "clientAuthenticatorType": "client-secret", - "secret": "secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "5f58a3c9-fb96-4565-aa97-24001d045829", - "clientId": "account", - "name": "${client_account}", - "baseUrl": "/auth/realms/c4po_realm_local/account", - "surrogateAuthRequired": false, - "enabled": true, - "clientAuthenticatorType": "client-secret", - "secret": "secret", - "defaultRoles": [ - "view-profile", - "manage-account" - ], - "redirectUris": [ - "/auth/realms/c4po_realm_local/account/*" - ], - "webOrigins": [ - "*" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.assertion.signature": "false", - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "saml.encrypt": "false", - "saml.server.signature": "false", - "saml.server.signature.keyinfo.ext": "false", - "exclude.session.state.from.auth.response": "false", - "saml_force_name_id_format": "false", - "saml.client.signature": "false", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "1f6ed582-3900-4f1d-8f5e-7e43ec70d721", - "clientId": "realm-management", - "name": "${client_realm-management}", - "surrogateAuthRequired": false, - "enabled": true, - "clientAuthenticatorType": "client-secret", - "secret": "secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "934c22be-97c3-44b3-a5c0-2f1bdc93e540", - "clientId": "broker", - "name": "${client_broker}", - "surrogateAuthRequired": false, - "enabled": true, - "clientAuthenticatorType": "client-secret", - "secret": "secret", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - } - ], - "clientScopes": [ - { - "id": "47257d76-9d63-49f5-a902-34df495a43a4", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${addressScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "66fb76f1-0cde-4791-9957-163fd18cb034", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } - } - ] - }, - { - "id": "075e5aee-2465-42a0-a653-b4d5273e273d", - "name": "email", - "description": "OpenID Connect built-in scope: email", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${emailScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "ef81e559-91a4-461c-871a-30848376ccb1", - "name": "email", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email", - "jsonType.label": "String" - } - }, - { - "id": "78100256-46c2-4fd9-94d5-b552d7baa837", - "name": "email verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "emailVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email_verified", - "jsonType.label": "boolean" - } - } - ] - }, - { - "id": "b5ebe782-a5bf-4cc6-8606-2ffe41c609af", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "33aeb362-e0a7-47ed-8f30-e257d262f2c7", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "multivalued": "true", - "userinfo.token.claim": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } - }, - { - "id": "6e3f86f8-6f52-4d0d-bcd6-62cd2b48217c", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "upn", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "df76f14a-1373-4866-b859-4ec4474150bf", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" - } - }, - { - "id": "ac5cf00b-0079-4bd0-8e3a-bf228ee622e5", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${phoneScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "c167feb2-2750-4916-9a3a-7a8b5bf8be20", - "name": "phone number", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumber", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number", - "jsonType.label": "String" - } - }, - { - "id": "e2f48d7b-be91-480c-a23b-77eb40b887bc", - "name": "phone number verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumberVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number_verified", - "jsonType.label": "boolean" - } - } - ] - }, - { - "id": "345ab109-6915-4487-b22b-625c3d416897", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${profileScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "1f743089-4d5c-48a3-91d0-db1ef3c12a14", - "name": "full name", - "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "id": "3de9048d-cc21-4c70-989f-d926d89b0766", - "name": "website", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "website", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "website", - "jsonType.label": "String" - } - }, - { - "id": "6988c27c-baf5-476d-b268-64175380ff15", - "name": "middle name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "middleName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "middle_name", - "jsonType.label": "String" - } - }, - { - "id": "cd8fce4a-c143-4093-bf04-a0a208f96ad8", - "name": "gender", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "gender", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "gender", - "jsonType.label": "String" - } - }, - { - "id": "774bb1ac-94dc-4024-869f-7ac3c74a3049", - "name": "given name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "firstName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "given_name", - "jsonType.label": "String" - } - }, - { - "id": "4fd53d8d-abf9-4cc7-accd-54d1fc836dfb", - "name": "picture", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "picture", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "picture", - "jsonType.label": "String" - } - }, - { - "id": "ec02c8fd-945e-4ca9-a4f4-063860c4c005", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - }, - { - "id": "282efa0d-e0b6-4eda-a16e-f46f9f2258dc", - "name": "zoneinfo", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "zoneinfo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "zoneinfo", - "jsonType.label": "String" - } - }, - { - "id": "03f06be8-0e88-479b-9c94-99221d84c2e9", - "name": "nickname", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "nickname", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "nickname", - "jsonType.label": "String" - } - }, - { - "id": "35423b7e-f94b-4ebc-a98a-7929cfc1ffc7", - "name": "updated at", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "updatedAt", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "updated_at", - "jsonType.label": "String" - } - }, - { - "id": "8a711ca2-9cc4-4f0d-9e68-e2fc8a584eca", - "name": "profile", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "profile", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "profile", - "jsonType.label": "String" - } - }, - { - "id": "3b1a7f22-88b0-4c50-aff7-b3e944d9c863", - "name": "birthdate", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "birthdate", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "birthdate", - "jsonType.label": "String" - } - }, - { - "id": "20ea37d6-3f1b-4744-a273-2894a8e5194b", - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "preferred_username", - "jsonType.label": "String" - } - }, - { - "id": "325e7a38-d5c3-4494-bda0-bd26b77e97f4", - "name": "family name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "lastName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "family_name", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "ca8687eb-111b-4004-84c8-3983b1e4c8f7", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "86dcf3ff-37bd-4e59-a873-8c48a84f3768", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "false", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } - } - ] - }, - { - "id": "3b49b3b4-bebc-446c-b5b5-181e5bf25f5a", - "name": "roles", - "description": "OpenID Connect scope for add user roles to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "true", - "consent.screen.text": "${rolesScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "290b8e1f-ab02-4c15-ad33-208d43b25f22", - "name": "realm roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "realm_access.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "d4161ff3-251b-4812-bbd2-47ecec67f2f0", - "name": "client roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-client-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "resource_access.${client_id}.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "03d5aaf1-d43b-4739-97ec-9906e034d09e", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ] - }, - { - "id": "5f39e087-9178-404d-be5a-81034370de82", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false", - "consent.screen.text": "" - }, - "protocolMappers": [ - { - "id": "7a931e44-5929-4b71-b40d-5bf96c7ee1f0", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": {} - } - ] - } - ], - "defaultDefaultClientScopes": [ - "email", - "profile", - "roles", - "web-origins", - "role_list" - ], - "defaultOptionalClientScopes": [ - "address", - "phone", - "microprofile-jwt", - "offline_access" - ], - "browserSecurityHeaders": { - "contentSecurityPolicyReportOnly": "", - "xContentTypeOptions": "nosniff", - "xRobotsTag": "none", - "xFrameOptions": "SAMEORIGIN", - "xXSSProtection": "1; mode=block", - "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "strictTransportSecurity": "max-age=31536000; includeSubDomains" - }, - "smtpServer": {}, - "eventsEnabled": false, - "eventsListeners": [ - "jboss-logging" - ], - "enabledEventTypes": [], - "adminEventsEnabled": false, - "adminEventsDetailsEnabled": false, - "components": { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ - { - "id": "eab6b4c5-33c3-4a40-8fa9-9d98ce0992c2", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": [ - "200" - ] - } - }, - { - "id": "f6203bd5-ba45-45df-bf45-741e0e2202f5", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "f7d196b9-3d1a-48af-82bc-8c3409209838", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-usermodel-property-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-full-name-mapper", - "saml-role-list-mapper", - "saml-user-property-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-address-mapper", - "saml-user-attribute-mapper" - ] - } - }, - { - "id": "90993401-7ea6-4213-8a53-d49ed7bebea0", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-full-name-mapper", - "oidc-usermodel-property-mapper", - "saml-role-list-mapper", - "saml-user-attribute-mapper", - "saml-user-property-mapper", - "oidc-address-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-sha256-pairwise-sub-mapper" - ] - } - }, - { - "id": "9ee10d51-fb23-4781-abd8-aed83ccbee4e", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "adad855f-8eb4-452b-9c01-f3df1d06dec2", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "9f64e8b7-b375-43a9-948b-49155901b906", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, - "config": { - "host-sending-registration-request-must-match": [ - "true" - ], - "client-uris-must-match": [ - "true" - ] - } - }, - { - "id": "5926dcbc-4191-4f1c-8c48-0414d0e4e959", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} - } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "5228af06-9a4a-4c1c-aafc-36b1e554ee61", - "name": "hmac-generated", - "providerId": "hmac-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ], - "algorithm": [ - "HS256" - ] - } - }, - { - "id": "e2223ecb-b36c-4e5d-ad0b-0e0f94aa5242", - "name": "rsa-generated", - "providerId": "rsa-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ] - } - }, - { - "id": "b37b8cac-96e4-4c1c-a9c5-1d44b9447412", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, - "config": { - "priority": [ - "100" - ] - } - } - ] - }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "04667e23-0791-4598-9e81-71dad2f8850e", - "alias": "Handle Existing Account", - "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-confirm-link", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "idp-email-verification", - "requirement": "ALTERNATIVE", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "ALTERNATIVE", - "priority": 30, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "d12d870d-a9c8-447d-9b2a-8513daaa1ff4", - "alias": "Verify Existing Account by Re-authentication", - "description": "Reauthentication of existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-username-password-form", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-otp-form", - "requirement": "OPTIONAL", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "a9d235f0-562f-44d8-8034-374b427375a4", - "alias": "browser", - "description": "browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-spnego", - "requirement": "DISABLED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "identity-provider-redirector", - "requirement": "ALTERNATIVE", - "priority": 25, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "ALTERNATIVE", - "priority": 30, - "flowAlias": "forms", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "0eb4370f-846b-4938-99a3-e41daf4b8f99", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-jwt", - "requirement": "ALTERNATIVE", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-secret-jwt", - "requirement": "ALTERNATIVE", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-x509", - "requirement": "ALTERNATIVE", - "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "6aa71b0a-0fbc-478f-9e70-3438a363d17d", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "direct-grant-validate-password", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "direct-grant-validate-otp", - "requirement": "OPTIONAL", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "9d882641-2247-4c39-bcdc-b6a731fb9d23", - "alias": "docker auth", - "description": "Used by Docker clients to authenticate against the IDP", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "docker-http-basic-authenticator", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "e4f1f2c7-4171-4b8b-8759-279bbf1be477", - "alias": "first broker login", - "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "review profile config", - "authenticator": "idp-review-profile", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticatorConfig": "create unique user config", - "authenticator": "idp-create-user-if-unique", - "requirement": "ALTERNATIVE", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "ALTERNATIVE", - "priority": 30, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "e00fbb15-36a1-483e-87ce-4b209b61ecfc", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-otp-form", - "requirement": "OPTIONAL", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "042387a5-bc8c-4dd7-9b87-9ff1f4e0ca1e", - "alias": "http challenge", - "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "no-cookie-redirect", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "basic-auth", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "basic-auth-otp", - "requirement": "DISABLED", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-spnego", - "requirement": "DISABLED", - "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "1c00ba91-5e86-4037-bd34-cea2aca0871b", - "alias": "registration", - "description": "registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "requirement": "REQUIRED", - "priority": 10, - "flowAlias": "registration form", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "51aaecfb-6135-4e8c-a154-98454e51b3f1", - "alias": "registration form", - "description": "registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-profile-action", - "requirement": "REQUIRED", - "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-password-action", - "requirement": "REQUIRED", - "priority": 50, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-recaptcha-action", - "requirement": "DISABLED", - "priority": 60, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "f1d62ae4-4a19-4f58-9301-dc014c064439", - "alias": "reset credentials", - "description": "Reset credentials for a user if they forgot their password or something", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "reset-credentials-choose-user", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-credential-email", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-password", - "requirement": "REQUIRED", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-otp", - "requirement": "OPTIONAL", - "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "e4405727-0f00-4b39-87b8-002d91b67579", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - } - ], - "authenticatorConfig": [ - { - "id": "bca740b1-4261-45b4-a91b-ef679c92a79d", - "alias": "create unique user config", - "config": { - "require.password.update.after.registration": "false" - } - }, - { - "id": "dd6821e3-694a-4e46-b284-1664ba568ade", - "alias": "review profile config", - "config": { - "update.profile.on.first.login": "missing" - } - } - ], - "requiredActions": [ - { - "alias": "CONFIGURE_TOTP", - "name": "Configure OTP", - "providerId": "CONFIGURE_TOTP", - "enabled": true, - "defaultAction": false, - "priority": 10, - "config": {} - }, - { - "alias": "terms_and_conditions", - "name": "Terms and Conditions", - "providerId": "terms_and_conditions", - "enabled": false, - "defaultAction": false, - "priority": 20, - "config": {} - }, - { - "alias": "UPDATE_PASSWORD", - "name": "Update Password", - "providerId": "UPDATE_PASSWORD", - "enabled": true, - "defaultAction": false, - "priority": 30, - "config": {} - }, - { - "alias": "UPDATE_PROFILE", - "name": "Update Profile", - "providerId": "UPDATE_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 40, - "config": {} - }, - { - "alias": "VERIFY_EMAIL", - "name": "Verify Email", - "providerId": "VERIFY_EMAIL", - "enabled": true, - "defaultAction": false, - "priority": 50, - "config": {} - } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "attributes": { - "_browser_header.xXSSProtection": "1; mode=block", - "_browser_header.xFrameOptions": "SAMEORIGIN", - "_browser_header.strictTransportSecurity": "max-age=31536000; includeSubDomains", - "permanentLockout": "false", - "quickLoginCheckMilliSeconds": "1000", - "_browser_header.xRobotsTag": "none", - "maxFailureWaitSeconds": "900", - "minimumQuickLoginWaitSeconds": "60", - "failureFactor": "30", - "actionTokenGeneratedByUserLifespan": "300", - "maxDeltaTimeSeconds": "43200", - "_browser_header.xContentTypeOptions": "nosniff", - "offlineSessionMaxLifespan": "5184000", - "actionTokenGeneratedByAdminLifespan": "43200", - "_browser_header.contentSecurityPolicyReportOnly": "", - "bruteForceProtected": "false", - "_browser_header.contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "waitIncrementSeconds": "60", - "offlineSessionMaxLifespanEnabled": "false" - }, - "keycloakVersion": "6.0.1", - "userManagedAccessAllowed": false -} \ No newline at end of file diff --git a/security-c4po-api/src/test/resources/realm-export.json b/security-c4po-api/src/test/resources/realm-export.json index 63addac..710b398 100644 --- a/security-c4po-api/src/test/resources/realm-export.json +++ b/security-c4po-api/src/test/resources/realm-export.json @@ -365,26 +365,51 @@ "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], "users" : [ { - "id" : "10e06d7a-8dd0-4ecd-8963-056b45079c4f", - "createdTimestamp" : 1617897245335, - "username" : "ttt", + "id" : "8f725a10-bdf5-4530-a185-4627fb092d78", + "createdTimestamp" : 1634988614562, + "username" : "test_admin", "enabled" : true, "totp" : false, - "emailVerified" : false, + "emailVerified" : true, "firstName" : "test", - "lastName" : "user", + "lastName" : "admin", + "email" : "testadmin@test.de", "credentials" : [ { - "id" : "7026fefc-ae26-442b-acae-92f1f2d24eac", + "id" : "52ec7433-9b1c-46f1-ae5b-4f9851d1424c", "type" : "password", - "createdDate" : 1617897287400, - "secretData" : "{\"value\":\"mhW4yxOg+8bcyPF4yWsfPZnLGUp4oaqc9aNA+WBcpr9qXgs/Jw+rM2VlLEgeD/kXGItcScA8V20sVGrMWT94Yw==\",\"salt\":\"nkH510WAwjKZJqd/ZEkIHA==\"}", + "createdDate" : 1634988624873, + "secretData" : "{\"value\":\"acmk/oJGV9GGtvrUjT1NJZPTizGvO9gvCj5tvzOSZKxW3OPwPYWfCE91OfmmLXOZcVddO8xdDufDLFliu52bxA==\",\"salt\":\"oHvWAC39azkfs7wJOwLdlQ==\"}", "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}" } ], "disableableCredentialTypes" : [ ], "requiredActions" : [ ], - "realmRoles" : [ "uma_authorization", "c4po_user", "c4po_admin" ], + "realmRoles" : [ "uma_authorization", "offline_access", "c4po_admin" ], + "clientRoles" : { + "account" : [ "view-profile", "manage-account" ] + }, + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "34869cfa-6454-4379-94d7-3a19fc2d98e0", + "createdTimestamp" : 1634988651658, + "username" : "test_user", + "enabled" : true, + "totp" : false, + "emailVerified" : true, + "firstName" : "test", + "lastName" : "user", + "email" : "testuser@test.de", + "credentials" : [ { + "id" : "58d1b3a1-7d28-4d40-b4cd-bfab0c9472d9", + "type" : "password", + "createdDate" : 1634988666427, + "secretData" : "{\"value\":\"IUz/GL+eof5R8MNEy4VB04sr1YBmyyy6/HdR0QFzKXRkhHcDWxO+vn1S3jy5n+iB/JBdFcrprI3/5rPgfyBiaA==\",\"salt\":\"l5BdVHeOO3RVvpxDWK2Jnw==\"}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "uma_authorization", "c4po_user", "offline_access" ], "clientRoles" : { - "c4po_local" : [ "user" ], "account" : [ "view-profile", "manage-account" ] }, "notBefore" : 0, @@ -1208,7 +1233,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper" ] } }, { "id" : "cc2d0cd7-3d3f-4b0a-ad95-7118f36bf188", @@ -1240,7 +1265,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "oidc-address-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper" ] } }, { "id" : "92230e65-7480-44c3-af2d-72ddee758cbc", @@ -1289,7 +1314,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "fa5fc78f-19a9-4737-868b-618163f28c79", + "id" : "9f2b9c09-a331-4126-9f89-5d459e911053", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -1309,7 +1334,7 @@ "autheticatorFlow" : true } ] }, { - "id" : "01735b0f-139f-46e5-bb63-f797a27efa77", + "id" : "6802ac9b-69cf-4838-99d4-747dd9de3f32", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -1335,7 +1360,7 @@ "autheticatorFlow" : false } ] }, { - "id" : "a7666cf0-626c-48c4-9e71-e408832de725", + "id" : "c1e6806d-ff33-46bc-be0f-70eb6924fc92", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -1355,7 +1380,7 @@ "autheticatorFlow" : false } ] }, { - "id" : "1dfabb7a-efdd-4964-bba5-389cad79b654", + "id" : "8b0f8be2-5933-4fc1-9546-484ecdf0766b", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -1375,7 +1400,7 @@ "autheticatorFlow" : false } ] }, { - "id" : "c3b2bf2b-3da8-430d-a9b7-8793c3dc30a3", + "id" : "d1b5d09c-36eb-40e2-8c1f-9b414e63b44e", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -1395,7 +1420,7 @@ "autheticatorFlow" : false } ] }, { - "id" : "44343bdf-8592-4242-835f-e349943a110b", + "id" : "ffeb5151-3357-4cd6-aa17-860c60de0cb3", "alias" : "Handle Existing Account", "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId" : "basic-flow", @@ -1415,7 +1440,7 @@ "autheticatorFlow" : true } ] }, { - "id" : "e72b8fcb-cd8b-4e7a-a057-3446b806b538", + "id" : "816cc72c-df79-49ee-b7a4-291f496e145b", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -1435,7 +1460,7 @@ "autheticatorFlow" : false } ] }, { - "id" : "2416145b-4d20-493c-bdf7-419898c002ee", + "id" : "5a782d53-4c32-437e-803a-a9e893b0f5eb", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -1456,7 +1481,7 @@ "autheticatorFlow" : true } ] }, { - "id" : "b7ff8aad-2daa-4736-8815-f3e8f0df391e", + "id" : "3faef6d7-2cfd-4190-a8d1-db1b8e953304", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -1476,7 +1501,7 @@ "autheticatorFlow" : true } ] }, { - "id" : "8339d3ba-2d0a-4d23-bbfa-a78e4973d3c9", + "id" : "5a57eb55-59f8-4d28-9003-ec350b4053cf", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -1508,7 +1533,7 @@ "autheticatorFlow" : true } ] }, { - "id" : "5ece002a-4e62-4d0d-8705-4b116164b424", + "id" : "1cf8aebb-27ae-4df5-96d4-82d6a0ba3bfd", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -1540,7 +1565,7 @@ "autheticatorFlow" : false } ] }, { - "id" : "bd27b0dc-bc87-40b7-a626-491b9955668d", + "id" : "1ec7e0bf-7bbe-4841-a21d-bfbc6cb492ae", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -1566,7 +1591,7 @@ "autheticatorFlow" : true } ] }, { - "id" : "2db79d60-7c9d-4516-80f0-0c5d60349899", + "id" : "d633bda1-5c4e-43c4-9a0e-776502549b57", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -1580,7 +1605,7 @@ "autheticatorFlow" : false } ] }, { - "id" : "25a92fbe-7d4d-46bc-a751-29ef844290a3", + "id" : "ac608b5f-4849-4830-a928-b51f996f77a8", "alias" : "first broker login", "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId" : "basic-flow", @@ -1601,7 +1626,7 @@ "autheticatorFlow" : true } ] }, { - "id" : "26f6a5db-9be8-446c-82d0-6f4e29b5f08d", + "id" : "b0731e4d-5935-4c29-8a4d-e7124e9eb164", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -1621,7 +1646,7 @@ "autheticatorFlow" : true } ] }, { - "id" : "05a94701-ad98-4bbc-a162-746a107afba5", + "id" : "b09ede5d-b7cd-4e9a-bb8d-e7caa880bc6d", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -1641,7 +1666,7 @@ "autheticatorFlow" : true } ] }, { - "id" : "75347884-d4cb-4eba-9b89-63566d509b92", + "id" : "96479ab9-2db1-47c9-ba6b-c9832e54937d", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -1656,7 +1681,7 @@ "autheticatorFlow" : true } ] }, { - "id" : "74e3a2d3-ecda-400d-8bff-0926dc272e4b", + "id" : "ae46ccfd-1bd2-4717-8380-de1040d7fe43", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -1688,7 +1713,7 @@ "autheticatorFlow" : false } ] }, { - "id" : "6eae8652-baf7-4a7d-80a4-1711906caec7", + "id" : "99d2284f-52e8-4c09-a500-6a7d1c5b108e", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -1720,7 +1745,7 @@ "autheticatorFlow" : true } ] }, { - "id" : "6135710b-b019-4117-ba32-578d3d496b2a", + "id" : "db4d353d-4b8d-4241-a09b-d4246590c82a", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -1735,13 +1760,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "3d3735a0-1362-4f0d-9306-bfc727da1b5b", + "id" : "1a64855a-6fe8-40df-90eb-bd936aed6e76", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "c1f4a15f-8234-4f0f-affa-baf610b001e1", + "id" : "e10161fb-9f1c-4c5c-ba84-5bcbf0dd358d", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/security-c4po-api/src/test/resources/script_local b/security-c4po-api/src/test/resources/script_local index f6c82e4..db262e0 100644 --- a/security-c4po-api/src/test/resources/script_local +++ b/security-c4po-api/src/test/resources/script_local @@ -4,11 +4,12 @@ sleep 20 ./kcadm.sh config credentials --server http://localhost:8888/auth --realm master --user admin --password admin USERID=$(./kcadm.sh create users -r c4po_realm_local -s username=test_admin \ --s email=Troy.Stewart@heros.com \ +-s email=testadmin@test.de \ -s firstName=test \ -s lastName=admin \ -s attributes.lang="de-DE" \ -s attributes.datenumberformat="en-US" \ +-s enabled=true \ -o --fields id | jq '.id' | tr -d '"') ./kcadm.sh update users/$USERID/reset-password -r c4po_realm_test -s type=password -s value=test -s temporary=false -n @@ -16,13 +17,14 @@ USERID=$(./kcadm.sh create users -r c4po_realm_local -s username=test_admin \ ./kcadm.sh add-roles -r c4po_realm_test --uusername test_admin --cclientid realm-management --rolename create-client --rolename view-users USERID=$(./kcadm.sh create users -r c4po_realm_local -s username=test_user \ --s email=Troy.Stewart@heros.com \ +-s email=testuser@test.de \ -s firstName=test \ -s lastName=user \ -s attributes.lang="de-DE" \ -s attributes.datenumberformat="en-US" \ +-s enabled=true \ -o --fields id | jq '.id' | tr -d '"') ./kcadm.sh update users/$USERID/reset-password -r c4po_realm_test -s type=password -s value=test -s temporary=false -n ./kcadm.sh add-roles --uusername test_user --rolename c4po_user -r c4po_realm_test -./kcadm.sh add-roles -r c4po_realm_test --uusername test_user --cclientid realm-management --rolename create-client --rolename view-users \ No newline at end of file +./kcadm.sh add-roles -r c4po_realm_test --uusername test_user --cclientid realm-management --rolename create-client --rolename view-users diff --git a/security-c4po-cfg/backend/docker-compose.backend.yml b/security-c4po-cfg/backend/docker-compose.backend.yml index 00983fc..80bc233 100644 --- a/security-c4po-cfg/backend/docker-compose.backend.yml +++ b/security-c4po-cfg/backend/docker-compose.backend.yml @@ -7,12 +7,6 @@ services: container_name: c4po-api environment: - SPRING_PROFILES_ACTIVE=COMPOSE - depends_on: - - c4po-db - - c4po-keycloak - links: - - c4po-db - - c4po-keycloak deploy: resources: limits: diff --git a/security-c4po-cfg/frontend/docker-compose.frontend.yml b/security-c4po-cfg/frontend/docker-compose.frontend.yml index d88f900..4bdbf1f 100644 --- a/security-c4po-cfg/frontend/docker-compose.frontend.yml +++ b/security-c4po-cfg/frontend/docker-compose.frontend.yml @@ -5,8 +5,6 @@ services: build: '../../security-c4po-angular' image: security-c4po-angular:latest container_name: c4po-angular - depends_on: - - c4po-keycloak deploy: resources: limits: diff --git a/security-c4po-cfg/kc/docker-compose.keycloak.yml b/security-c4po-cfg/kc/docker-compose.keycloak.yml index ac3b62d..050428c 100644 --- a/security-c4po-cfg/kc/docker-compose.keycloak.yml +++ b/security-c4po-cfg/kc/docker-compose.keycloak.yml @@ -3,8 +3,6 @@ version: '3.1' services: c4po-keycloak: container_name: c4po-keycloak - depends_on: - - c4po-keycloak-postgres image: jboss/keycloak:11.0.3 volumes: - ../cfg/c4po_realm_export.json:/tmp/c4po_realm_export.json