feat: added Keycloak and WebsecurityConfig and improved script management
This commit is contained in:
parent
41e375c32e
commit
dfc5e1a934
35
c4po.sh
35
c4po.sh
|
@ -1,6 +1,12 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
baseDir=$(pwd)"/"
|
docker_reg="c4po.io"
|
||||||
echo"
|
baseDir=$(pwd)
|
||||||
|
|
||||||
|
composeKeycloak=$baseDir"/security-c4po-cfg/kc/docker-compose.keycloak.yml"
|
||||||
|
composeFrontend=$baseDir"/security-c4po-cfg/frontend/docker-compose.frontend.yml"
|
||||||
|
composeBackend=$baseDir"/security-c4po-cfg/backend/docker-compose.backend.yml"
|
||||||
|
|
||||||
|
echo -e "
|
||||||
_______ _______ _______ _ _ ______ _____ _______ __ __
|
_______ _______ _______ _ _ ______ _____ _______ __ __
|
||||||
|______ |______ | | | |_____/ | | \_/
|
|______ |______ | | | |_____/ | | \_/
|
||||||
______| |______ |_____ |_____| | \_ __|__ | | _/_/_/ _/ _/ _/_/_/ _/_/
|
______| |______ |_____ |_____| | \_ __|__ | | _/_/_/ _/ _/ _/_/_/ _/_/
|
||||||
|
@ -8,6 +14,25 @@ ______| |______ |_____ |_____| | \_ __|__ | | _/_/_/ _/
|
||||||
_/ _/_/_/_/ _/_/_/ _/ _/
|
_/ _/_/_/_/ _/_/_/ _/ _/
|
||||||
_/ _/ _/ _/ _/
|
_/ _/ _/ _/ _/
|
||||||
_/_/_/ _/ _/ _/_/
|
_/_/_/ _/ _/ _/_/
|
||||||
"
|
\n"
|
||||||
#docker-compose up --build
|
|
||||||
docker-compose up
|
echo "-------------CLEAN UP Container---------------"
|
||||||
|
echo -e "\n"
|
||||||
|
#docker rm -f security-c4po-keycloak
|
||||||
|
#docker rm -f security-c4po-postgres-keycloak
|
||||||
|
docker rm -f security-c4po-api
|
||||||
|
docker rm -f security-c4po-angular
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
echo "-----------------Start Build------------------"
|
||||||
|
echo -e "\n"
|
||||||
|
echo " - Backend: "
|
||||||
|
docker-compose -f ${composeBackend} build
|
||||||
|
echo -e "\n"
|
||||||
|
echo " - Frontend: "
|
||||||
|
docker-compose -f ${composeFrontend} build
|
||||||
|
echo -e "\n"
|
||||||
|
|
||||||
|
echo "------------Start Docker Container------------"
|
||||||
|
echo -e "\n"
|
||||||
|
docker-compose -f ${composeKeycloak} -f ${composeBackend} -f ${composeFrontend} up
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
version: '3.1'
|
|
||||||
|
|
||||||
services:
|
|
||||||
api:
|
|
||||||
build: './security-c4po-api'
|
|
||||||
image: security-c4po-api:latest
|
|
||||||
container_name: security-c4po-api
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: "1G"
|
|
||||||
ports:
|
|
||||||
- '8443:8443'
|
|
||||||
|
|
||||||
angular:
|
|
||||||
build: './security-c4po-angular'
|
|
||||||
image: security-c4po-angular:latest
|
|
||||||
container_name: security-c4po-angular
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: "1G"
|
|
||||||
ports:
|
|
||||||
- '4200:4200'
|
|
|
@ -8610,6 +8610,11 @@
|
||||||
"supports-color": "^7.0.0"
|
"supports-color": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"js-sha256": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
|
||||||
|
},
|
||||||
"js-tokens": {
|
"js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
@ -8782,6 +8787,11 @@
|
||||||
"set-immediate-shim": "~1.0.1"
|
"set-immediate-shim": "~1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jwt-decode": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||||
|
},
|
||||||
"karma-source-map-support": {
|
"karma-source-map-support": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz",
|
||||||
|
@ -8791,6 +8801,30 @@
|
||||||
"source-map-support": "^0.5.5"
|
"source-map-support": "^0.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"keycloak-angular": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/keycloak-angular/-/keycloak-angular-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-FNIZBVKI3QNw0ucHnSjDDe8859WT6NtVlsKtCvJzAS9mFiYCDFDT9cRWt9On2aFu39rGyBEBNbpsTE1Mso48NQ==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keycloak-js": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-XMbppXjkkFmt88vR8jrxH32dz/dmFETDObD6NzLAT3HgpC9Thi6LSEUm7XsROq4z+2i/qLwlWTHbgLXj6LxBrg==",
|
||||||
|
"requires": {
|
||||||
|
"base64-js": "1.3.1",
|
||||||
|
"js-sha256": "0.9.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"killable": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
|
|
|
@ -35,6 +35,9 @@
|
||||||
"@ngxs/store": "^3.7.0",
|
"@ngxs/store": "^3.7.0",
|
||||||
"eva-icons": "^1.1.3",
|
"eva-icons": "^1.1.3",
|
||||||
"i18n-iso-countries": "^6.2.2",
|
"i18n-iso-countries": "^6.2.2",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
|
"keycloak-angular": "^8.1.0",
|
||||||
|
"keycloak-js": "^13.0.0",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"moment-timezone": "latest",
|
"moment-timezone": "latest",
|
||||||
"ngx-moment": "^5.0.0",
|
"ngx-moment": "^5.0.0",
|
||||||
|
|
|
@ -2,10 +2,8 @@ import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
import {HomeComponent} from './home/home.component';
|
import {HomeComponent} from './home/home.component';
|
||||||
import {AuthGuardService} from '../shared/guards/auth-guard.service';
|
import {AuthGuardService} from '../shared/guards/auth-guard.service';
|
||||||
import {LoginGuardService} from '../shared/guards/login-guard.service';
|
|
||||||
|
|
||||||
export const START_PAGE = 'home';
|
export const START_PAGE = 'home';
|
||||||
export const FALLBACK_PAGE = 'home';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -18,11 +16,12 @@ const routes: Routes = [
|
||||||
loadChildren: () => import('./dashboard').then(mod => mod.DashboardModule),
|
loadChildren: () => import('./dashboard').then(mod => mod.DashboardModule),
|
||||||
canActivate: [AuthGuardService]
|
canActivate: [AuthGuardService]
|
||||||
},
|
},
|
||||||
{
|
// ToDo: Exchange default Keycloak login with self made login
|
||||||
|
/*{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
loadChildren: () => import('./login').then(mod => mod.LoginModule),
|
loadChildren: () => import('./login').then(mod => mod.LoginModule),
|
||||||
canActivate: [LoginGuardService]
|
canActivate: [LoginGuardService]
|
||||||
},
|
},*/
|
||||||
{path: '**', redirectTo: START_PAGE},
|
{path: '**', redirectTo: START_PAGE},
|
||||||
{path: '', redirectTo: START_PAGE, pathMatch: 'full'},
|
{path: '', redirectTo: START_PAGE, pathMatch: 'full'},
|
||||||
];
|
];
|
||||||
|
|
|
@ -11,6 +11,7 @@ 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 {NgxsModule} from '@ngxs/store';
|
||||||
import {HeaderModule} from './header/header.module';
|
import {HeaderModule} from './header/header.module';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -32,6 +33,9 @@ describe('AppComponent', () => {
|
||||||
NgxsModule.forRoot([SessionState]),
|
NgxsModule.forRoot([SessionState]),
|
||||||
HttpClientTestingModule
|
HttpClientTestingModule
|
||||||
],
|
],
|
||||||
|
providers: [
|
||||||
|
KeycloakService
|
||||||
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent
|
AppComponent
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {NgModule} from '@angular/core';
|
import {APP_INITIALIZER, NgModule} from '@angular/core';
|
||||||
import {BrowserModule} from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
import {AppRoutingModule} from './app-routing.module';
|
import {AppRoutingModule} from './app-routing.module';
|
||||||
import {AppComponent} from './app.component';
|
import {AppComponent} from './app.component';
|
||||||
|
@ -23,6 +23,8 @@ import {NotificationService} from '../shared/services/notification.service';
|
||||||
import {ThemeModule} from '@assets/@theme/theme.module';
|
import {ThemeModule} from '@assets/@theme/theme.module';
|
||||||
import {HeaderModule} from './header/header.module';
|
import {HeaderModule} from './header/header.module';
|
||||||
import {HomeModule} from './home/home.module';
|
import {HomeModule} from './home/home.module';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
import {httpInterceptorProviders} from '../shared/interceptors';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -55,9 +57,19 @@ import {HomeModule} from './home/home.module';
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
HttpClient,
|
HttpClient,
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: initializer,
|
||||||
|
multi: true,
|
||||||
|
deps: [KeycloakService]
|
||||||
|
},
|
||||||
|
KeycloakService,
|
||||||
|
httpInterceptorProviders,
|
||||||
NotificationService
|
NotificationService
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [
|
||||||
|
AppComponent
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
constructor(library: FaIconLibrary, faConfig: FaConfig) {
|
constructor(library: FaIconLibrary, faConfig: FaConfig) {
|
||||||
|
@ -65,3 +77,30 @@ export class AppModule {
|
||||||
faConfig.defaultPrefix = 'fas';
|
faConfig.defaultPrefix = 'fas';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function initializer(keycloak: KeycloakService): () => Promise<any> {
|
||||||
|
return async (): Promise<any> => {
|
||||||
|
try {
|
||||||
|
await keycloak.init({
|
||||||
|
config: {
|
||||||
|
url: environment.keycloakURL,
|
||||||
|
realm: environment.keycloakrealm,
|
||||||
|
clientId: environment.keycloakclientId
|
||||||
|
},
|
||||||
|
initOptions: {
|
||||||
|
onLoad: 'login-required',
|
||||||
|
checkLoginIframe: false,
|
||||||
|
// flow: 'implicit'
|
||||||
|
},
|
||||||
|
loadUserProfileAtStartUp: false,
|
||||||
|
enableBearerInterceptor: true,
|
||||||
|
bearerExcludedUrls: [
|
||||||
|
'/assets',
|
||||||
|
'/clients/public'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,9 @@ describe('DashboardComponent', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [ DashboardComponent ]
|
declarations: [
|
||||||
|
DashboardComponent
|
||||||
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { HomeComponent } from './home.component';
|
import { HomeComponent } from './home.component';
|
||||||
import {NbButtonModule, NbCardModule} from '@nebular/theme';
|
import {NbButtonModule, NbCardModule} from '@nebular/theme';
|
||||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
||||||
describe('HomeComponent', () => {
|
describe('HomeComponent', () => {
|
||||||
let component: HomeComponent;
|
let component: HomeComponent;
|
||||||
|
@ -17,6 +18,9 @@ describe('HomeComponent', () => {
|
||||||
HttpClientTestingModule,
|
HttpClientTestingModule,
|
||||||
NbCardModule,
|
NbCardModule,
|
||||||
NbButtonModule
|
NbButtonModule
|
||||||
|
],
|
||||||
|
providers : [
|
||||||
|
KeycloakService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {CommonModule} from '@angular/common';
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
import {NotificationService} from '../../shared/services/notification.service';
|
import {NotificationService} from '../../shared/services/notification.service';
|
||||||
import {NotificationServiceMock} from '../../shared/services/notification.service.mock';
|
import {NotificationServiceMock} from '../../shared/services/notification.service.mock';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
||||||
const DESIRED_STORE_STATE_SESSION: SessionStateModel = {
|
const DESIRED_STORE_STATE_SESSION: SessionStateModel = {
|
||||||
userAccount: {
|
userAccount: {
|
||||||
|
@ -68,6 +69,7 @@ describe('LoginComponent', () => {
|
||||||
LoginComponent
|
LoginComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
KeycloakService,
|
||||||
{provide: NotificationService, useValue: new NotificationServiceMock()}
|
{provide: NotificationService, useValue: new NotificationServiceMock()}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,12 +10,14 @@ import {UpdateIsAuthenticated, UpdateUser} from '../../shared/stores/session-sta
|
||||||
import {GlobalTitlesVariables} from '../../shared/config/global-variables';
|
import {GlobalTitlesVariables} from '../../shared/config/global-variables';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
import {FieldStatus} from '../../shared/models/form-field-status.model';
|
import {FieldStatus} from '../../shared/models/form-field-status.model';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrls: ['./login.component.scss']
|
styleUrls: ['./login.component.scss']
|
||||||
})
|
})
|
||||||
|
// ToDo: Exchange default Keycloak login with self made login
|
||||||
export class LoginComponent implements OnInit, OnDestroy {
|
export class LoginComponent implements OnInit, OnDestroy {
|
||||||
readonly MIN_LENGTH: number = 2;
|
readonly MIN_LENGTH: number = 2;
|
||||||
readonly SECURITYC4PO_TITLE = GlobalTitlesVariables.SECURITYC4PO_TITLE;
|
readonly SECURITYC4PO_TITLE = GlobalTitlesVariables.SECURITYC4PO_TITLE;
|
||||||
|
@ -41,7 +43,8 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private store: Store,
|
private store: Store,
|
||||||
private readonly httpClient: HttpClient,
|
private readonly httpClient: HttpClient,
|
||||||
private notificationService: NotificationService) {
|
private notificationService: NotificationService,
|
||||||
|
protected keycloakService: KeycloakService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -65,8 +68,9 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||||
login(): void {
|
login(): void {
|
||||||
const username = this.loginUsernameCtrl.value;
|
const username = this.loginUsernameCtrl.value;
|
||||||
const password = this.loginPasswordCtrl.value;
|
const password = this.loginPasswordCtrl.value;
|
||||||
if (username === DefaultUser.username
|
// ToDo: Should be handled in Guards
|
||||||
&& password === DefaultUser.password) {
|
this.keycloakService.login({});
|
||||||
|
if (true) {
|
||||||
// ToDo: Should be handled in Guards
|
// ToDo: Should be handled in Guards
|
||||||
this.store.dispatch(new UpdateIsAuthenticated(true));
|
this.store.dispatch(new UpdateIsAuthenticated(true));
|
||||||
this.store.dispatch(new UpdateUser(this.user, true));
|
this.store.dispatch(new UpdateUser(this.user, true));
|
||||||
|
@ -120,10 +124,3 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||||
export interface Version {
|
export interface Version {
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DefaultUser {
|
|
||||||
username = 'ttt',
|
|
||||||
password = 'Test1234!'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: true
|
production: true,
|
||||||
|
|
||||||
|
// keycloak
|
||||||
|
keycloakURL: 'http://localhost:8888/auth',
|
||||||
|
keycloakrealm: 'c4po_realm_local',
|
||||||
|
keycloakclientId: 'c4po_local',
|
||||||
|
|
||||||
|
// backend service
|
||||||
|
apiEndpoint: 'http://localhost:8443',
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,13 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
stage: 'n/a',
|
stage: 'n/a',
|
||||||
production: false,
|
production: false,
|
||||||
|
|
||||||
|
// keycloak
|
||||||
|
keycloakURL: 'http://localhost:8888/auth',
|
||||||
|
keycloakrealm: 'c4po_realm_local',
|
||||||
|
keycloakclientId: 'c4po_local',
|
||||||
|
|
||||||
|
// backend service
|
||||||
apiEndpoint: 'http://localhost:8443',
|
apiEndpoint: 'http://localhost:8443',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,45 +4,42 @@ import {Store} from '@ngxs/store';
|
||||||
import {Observable, of} from 'rxjs';
|
import {Observable, of} from 'rxjs';
|
||||||
import {SessionState} from '../stores/session-state/session-state';
|
import {SessionState} from '../stores/session-state/session-state';
|
||||||
import {catchError, map} from 'rxjs/operators';
|
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({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AuthGuardService implements CanActivate {
|
export class AuthGuardService extends KeycloakAuthGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly router: Router,
|
public readonly router: Router,
|
||||||
|
protected keycloakService: KeycloakService,
|
||||||
private readonly store: Store) {
|
private readonly store: Store) {
|
||||||
|
super(router, keycloakService);
|
||||||
}
|
}
|
||||||
|
|
||||||
canActivate(
|
isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
|
||||||
next: ActivatedRouteSnapshot,
|
return new Promise((resolve, reject) => {
|
||||||
state: RouterStateSnapshot): Observable<boolean> {
|
if (!this.authenticated) {
|
||||||
return this.isAuthenticated()
|
this.keycloakAngular.login()
|
||||||
.pipe(
|
.catch(e => console.error(e));
|
||||||
map((canAccess: boolean) => {
|
return reject(false);
|
||||||
if (canAccess) {
|
}
|
||||||
return canAccess;
|
|
||||||
} else {
|
|
||||||
this.router.navigate(['/login']);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const requiredRoles: string[] = route.data.roles;
|
||||||
* @return boolean
|
if (!requiredRoles || requiredRoles.length === 0) {
|
||||||
*/
|
this.store.dispatch(new UpdateIsAuthenticated(true));
|
||||||
private isAuthenticated(): Observable<boolean> {
|
this.store.dispatch(new UpdateUser(route.data.user, true));
|
||||||
// ToDo: Should check from Authentication Provider
|
return resolve(true);
|
||||||
return of(this.store.selectSnapshot(SessionState.isAuthenticated))
|
} else {
|
||||||
.pipe(
|
if (!this.roles || this.roles.length === 0) {
|
||||||
map((isLoggedIn: boolean) => {
|
this.store.dispatch(new UpdateIsAuthenticated(false));
|
||||||
return isLoggedIn;
|
this.store.dispatch(new UpdateUser(null, true));
|
||||||
}),
|
resolve(false);
|
||||||
catchError(() => {
|
}
|
||||||
return of(false);
|
resolve(requiredRoles.every(role => this.roles.indexOf(role) > -1));
|
||||||
})
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {SessionState} from '../stores/session-state/session-state';
|
||||||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||||
import {HttpLoaderFactory} from '../../app/common-app.module';
|
import {HttpLoaderFactory} from '../../app/common-app.module';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
||||||
describe('LoginGuardService', () => {
|
describe('LoginGuardService', () => {
|
||||||
let service: LoginGuardService;
|
let service: LoginGuardService;
|
||||||
|
@ -27,6 +28,7 @@ describe('LoginGuardService', () => {
|
||||||
NgxsModule.forRoot([SessionState])
|
NgxsModule.forRoot([SessionState])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
KeycloakService
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
service = TestBed.inject(LoginGuardService);
|
service = TestBed.inject(LoginGuardService);
|
||||||
|
|
|
@ -4,18 +4,45 @@ import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '
|
||||||
import {Observable, of} from 'rxjs';
|
import {Observable, of} from 'rxjs';
|
||||||
import {catchError, map, tap} from 'rxjs/operators';
|
import {catchError, map, tap} from 'rxjs/operators';
|
||||||
import {SessionState} from '../stores/session-state/session-state';
|
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({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class LoginGuardService implements CanActivate {
|
export class LoginGuardService extends KeycloakAuthGuard implements CanActivate {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
public readonly router: Router,
|
||||||
private store: Store) {
|
protected keycloakAngular: KeycloakService,
|
||||||
|
private readonly store: Store) {
|
||||||
|
super(router, keycloakAngular);
|
||||||
}
|
}
|
||||||
|
|
||||||
canActivate(routeSnapshot: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
|
||||||
|
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<boolean> {
|
||||||
return this.isAuthenticated().pipe(
|
return this.isAuthenticated().pipe(
|
||||||
tap((canAccess: boolean) => {
|
tap((canAccess: boolean) => {
|
||||||
if (canAccess) {
|
if (canAccess) {
|
||||||
|
@ -27,9 +54,9 @@ export class LoginGuardService implements CanActivate {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/!**
|
||||||
* @return state of authentication
|
* @return state of authentication
|
||||||
*/
|
*!/
|
||||||
private isAuthenticated(): Observable<boolean> {
|
private isAuthenticated(): Observable<boolean> {
|
||||||
// ToDo: Should check from Authentication Provider
|
// ToDo: Should check from Authentication Provider
|
||||||
return of(this.store.selectSnapshot(SessionState.isAuthenticated))
|
return of(this.store.selectSnapshot(SessionState.isAuthenticated))
|
||||||
|
@ -41,6 +68,6 @@ export class LoginGuardService implements CanActivate {
|
||||||
return of(false);
|
return of(false);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import {HTTP_INTERCEPTORS} from '@angular/common/http';
|
||||||
|
import {TokenInterceptor} from './token.interceptor';
|
||||||
|
|
||||||
|
export const httpInterceptorProviders = [
|
||||||
|
{provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true},
|
||||||
|
];
|
|
@ -0,0 +1,72 @@
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
import {environment} from '../../environments/environment';
|
||||||
|
import {Observable, Subscriber} from 'rxjs';
|
||||||
|
import {mergeMap} from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TokenInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
constructor(private keycloakService: KeycloakService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: has to be edit every time a service is added and requires the keycloak token on HEADER
|
||||||
|
*/
|
||||||
|
private static listOfKeycloakRelevantHosts(): { origin: string }[] {
|
||||||
|
const relevantList = new Array<{ origin: string }>();
|
||||||
|
relevantList.push({origin: getOriginByUrl(environment.apiEndpoint)});
|
||||||
|
relevantList.push({origin: getOriginByUrl(environment.keycloakURL)});
|
||||||
|
return relevantList;
|
||||||
|
|
||||||
|
function getOriginByUrl(inputUrl: string): string {
|
||||||
|
return (new URL(inputUrl)).origin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static requestTargetIsWhitelisted(requestTargetOrigin: string): boolean {
|
||||||
|
if (requestTargetOrigin) {
|
||||||
|
try {
|
||||||
|
const targetUrl: URL = new URL(requestTargetOrigin);
|
||||||
|
if (targetUrl && targetUrl.origin) {
|
||||||
|
const matchList = TokenInterceptor.listOfKeycloakRelevantHosts()
|
||||||
|
.map(value => value.origin)
|
||||||
|
.filter(value => (value === targetUrl.origin));
|
||||||
|
return !!matchList.length;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore e.g. local calls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
const requestTargetHost = request.url || '';
|
||||||
|
|
||||||
|
if (TokenInterceptor.requestTargetIsWhitelisted(requestTargetHost)) {
|
||||||
|
const tokenObserver: Observable<string> = new Observable((observer: Subscriber<any>): void => {
|
||||||
|
this.keycloakService.getToken().then(token => {
|
||||||
|
observer.next(token);
|
||||||
|
observer.complete();
|
||||||
|
}).catch(error => {
|
||||||
|
observer.error(error);
|
||||||
|
observer.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return tokenObserver.pipe(
|
||||||
|
mergeMap((authToken: string) => {
|
||||||
|
request = request.clone({
|
||||||
|
headers: request.headers.append('Authorization', `Bearer ${authToken}`)
|
||||||
|
});
|
||||||
|
return next.handle(request);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// Do nothing
|
||||||
|
return next.handle(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
import { v4 as UUID } from 'uuid';
|
import { v4 as UUID } from 'uuid';
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
id: string;
|
id?: string;
|
||||||
username: string;
|
username?: string;
|
||||||
firstName: string;
|
firstName?: string;
|
||||||
lastName: string;
|
lastName?: string;
|
||||||
mailAddress: string;
|
mailAddress?: string;
|
||||||
interfaceLang: string;
|
interfaceLang?: string;
|
||||||
|
|
||||||
constructor(username?: string,
|
constructor(username?: string,
|
||||||
firstName?: string,
|
firstName?: string,
|
||||||
|
@ -17,7 +17,15 @@ export class User {
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.firstName = firstName;
|
this.firstName = firstName;
|
||||||
this.lastName = lastName;
|
this.lastName = lastName;
|
||||||
this.mailAddress = email;
|
if (email) {
|
||||||
this.interfaceLang = interfaceLang;
|
this.mailAddress = email;
|
||||||
|
} else {
|
||||||
|
this.mailAddress = null;
|
||||||
|
}
|
||||||
|
if (interfaceLang) {
|
||||||
|
this.interfaceLang = interfaceLang;
|
||||||
|
} else {
|
||||||
|
this.interfaceLang = 'en-US';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {HttpLoaderFactory} from '../../app/common-app.module';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
import {NgxsModule} from '@ngxs/store';
|
import {NgxsModule} from '@ngxs/store';
|
||||||
import {SessionState} from '../stores/session-state/session-state';
|
import {SessionState} from '../stores/session-state/session-state';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
||||||
describe('NotificationService', () => {
|
describe('NotificationService', () => {
|
||||||
let toastrServiceStub: Partial<NbToastrService>;
|
let toastrServiceStub: Partial<NbToastrService>;
|
||||||
|
@ -45,6 +46,7 @@ describe('NotificationService', () => {
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
NotificationService,
|
NotificationService,
|
||||||
|
KeycloakService,
|
||||||
{provide: NbToastrService, useValue: toastrServiceStub},
|
{provide: NbToastrService, useValue: toastrServiceStub},
|
||||||
{provide: TranslateService, useValue: translateServiceStub}]
|
{provide: TranslateService, useValue: translateServiceStub}]
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { TestBed } from '@angular/core/testing';
|
||||||
import { ProjectService } from './project.service';
|
import { ProjectService } from './project.service';
|
||||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
||||||
describe('ProjectService', () => {
|
describe('ProjectService', () => {
|
||||||
let service: ProjectService;
|
let service: ProjectService;
|
||||||
|
@ -13,7 +14,9 @@ describe('ProjectService', () => {
|
||||||
HttpClientTestingModule,
|
HttpClientTestingModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
],
|
],
|
||||||
providers: []
|
providers: [
|
||||||
|
KeycloakService
|
||||||
|
]
|
||||||
});
|
});
|
||||||
service = TestBed.inject(ProjectService);
|
service = TestBed.inject(ProjectService);
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {Observable} from 'rxjs';
|
||||||
})
|
})
|
||||||
export class ProjectService {
|
export class ProjectService {
|
||||||
|
|
||||||
private apiBaseURL = `${environment.apiEndpoint}/v1/projects`;
|
private apiBaseURL = `${environment.apiEndpoint}/projects`;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {HttpLoaderFactory} from '../../app/common-app.module';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
import {NgxsModule} from '@ngxs/store';
|
import {NgxsModule} from '@ngxs/store';
|
||||||
import {SessionState} from '../stores/session-state/session-state';
|
import {SessionState} from '../stores/session-state/session-state';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
||||||
describe('UserService', () => {
|
describe('UserService', () => {
|
||||||
let service: UserService;
|
let service: UserService;
|
||||||
|
@ -26,7 +27,9 @@ describe('UserService', () => {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: []
|
providers: [
|
||||||
|
KeycloakService
|
||||||
|
]
|
||||||
});
|
});
|
||||||
service = TestBed.inject(UserService);
|
service = TestBed.inject(UserService);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||||
import {User} from '../models/user.model';
|
import {User} from '../models/user.model';
|
||||||
import {Observable} from 'rxjs';
|
import {from, Observable, Subscriber} from 'rxjs';
|
||||||
import {Store} from '@ngxs/store';
|
import {Store} from '@ngxs/store';
|
||||||
import {SessionState} from '../stores/session-state/session-state';
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
import {map} from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -11,6 +12,7 @@ import {SessionState} from '../stores/session-state/session-state';
|
||||||
export class UserService {
|
export class UserService {
|
||||||
|
|
||||||
constructor(private http: HttpClient,
|
constructor(private http: HttpClient,
|
||||||
|
private keycloakService: KeycloakService,
|
||||||
private store: Store) {
|
private store: Store) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +22,33 @@ export class UserService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentAuthenticatedUser(): Observable<User> {
|
private createHttpOptions(): Observable<any> {
|
||||||
return this.store.select(SessionState.userAccount);
|
return this.getToken().pipe(
|
||||||
|
// create HttpHeaders
|
||||||
|
map((token: string): HttpHeaders => {
|
||||||
|
return UserService.createHttpHeadersWithContentType(token);
|
||||||
|
}),
|
||||||
|
// createHttpOptions
|
||||||
|
map((httpHeaders: HttpHeaders): { headers } => {
|
||||||
|
return {headers: httpHeaders};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadUserProfile(): Observable<User> {
|
||||||
|
return from(this.keycloakService.loadUserProfile()) as Observable<User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getToken(): Observable<string> {
|
||||||
|
return new Observable((observer: Subscriber<any>): void => {
|
||||||
|
this.keycloakService.getToken().then(token => {
|
||||||
|
console.warn(token);
|
||||||
|
observer.next(token);
|
||||||
|
observer.complete();
|
||||||
|
}).catch(error => {
|
||||||
|
observer.error(error);
|
||||||
|
observer.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,10 @@ export class ResetSession {
|
||||||
static readonly type = '[Session] ResetSession';
|
static readonly type = '[Session] ResetSession';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FetchUser {
|
||||||
|
static readonly type = '[Session] FetchUser';
|
||||||
|
}
|
||||||
|
|
||||||
export class UpdateUser {
|
export class UpdateUser {
|
||||||
static readonly type = '[Session] UpdateUser';
|
static readonly type = '[Session] UpdateUser';
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {HttpClient} from '@angular/common/http';
|
||||||
import {SESSION_STATE_NAME, SessionState, SessionStateModel} from './session-state';
|
import {SESSION_STATE_NAME, SessionState, SessionStateModel} from './session-state';
|
||||||
import {User} from '../../models/user.model';
|
import {User} from '../../models/user.model';
|
||||||
import {InitSession, UpdateUser} from './session-state.actions';
|
import {InitSession, UpdateUser} from './session-state.actions';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
||||||
const INITIAL_STORE_STATE_SESSION: SessionStateModel = {
|
const INITIAL_STORE_STATE_SESSION: SessionStateModel = {
|
||||||
userAccount: {
|
userAccount: {
|
||||||
|
@ -40,7 +41,9 @@ describe('SessionState', () => {
|
||||||
}),
|
}),
|
||||||
NgxsModule.forRoot([SessionState]),
|
NgxsModule.forRoot([SessionState]),
|
||||||
],
|
],
|
||||||
providers: []
|
providers: [
|
||||||
|
KeycloakService
|
||||||
|
]
|
||||||
});
|
});
|
||||||
store = TestBed.inject(Store);
|
store = TestBed.inject(Store);
|
||||||
store.reset({
|
store.reset({
|
||||||
|
|
|
@ -2,9 +2,10 @@ import {User} from '../../models/user.model';
|
||||||
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
|
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
|
||||||
import {Action, Selector, State, StateContext} from '@ngxs/store';
|
import {Action, Selector, State, StateContext} from '@ngxs/store';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {InitSession, ResetSession, UpdateIsAuthenticated, UpdateUser, UpdateUserSettings} from './session-state.actions';
|
import {FetchUser, InitSession, ResetSession, UpdateIsAuthenticated, UpdateUser, UpdateUserSettings} from './session-state.actions';
|
||||||
import deepEqual from 'deep-equal';
|
import deepEqual from 'deep-equal';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import {UserService} from '../../services/user.service';
|
||||||
|
|
||||||
export interface SessionStateModel {
|
export interface SessionStateModel {
|
||||||
userAccount: User;
|
userAccount: User;
|
||||||
|
@ -24,6 +25,7 @@ export const SESSION_STORAGE_KEY_USER = 'user';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SessionState {
|
export class SessionState {
|
||||||
constructor(@Inject(LOCALE_ID) private readonly localeId: string,
|
constructor(@Inject(LOCALE_ID) private readonly localeId: string,
|
||||||
|
private readonly userService: UserService,
|
||||||
private readonly translateService: TranslateService) {
|
private readonly translateService: TranslateService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +53,17 @@ export class SessionState {
|
||||||
ctx.dispatch(new InitSession());
|
ctx.dispatch(new InitSession());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Action(FetchUser)
|
||||||
|
fetchUser(ctx: StateContext<SessionStateModel>): void {
|
||||||
|
this.userService.loadUserProfile().subscribe({
|
||||||
|
next: (user: User): void => {
|
||||||
|
ctx.dispatch(new UpdateUser(user, true));
|
||||||
|
},
|
||||||
|
// TODO: add better error handling
|
||||||
|
error: (err) => console.error('Failed to load UserProfile', err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Action(UpdateUser)
|
@Action(UpdateUser)
|
||||||
updateUser(ctx: StateContext<SessionStateModel>, {user, force}: UpdateUser): void {
|
updateUser(ctx: StateContext<SessionStateModel>, {user, force}: UpdateUser): void {
|
||||||
const state = ctx.getState();
|
const state = ctx.getState();
|
||||||
|
|
|
@ -61,6 +61,7 @@ dependencies {
|
||||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:2.11.3")
|
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:2.11.3")
|
||||||
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions:1.1.1")
|
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions:1.1.1")
|
||||||
implementation("javax.websocket:javax.websocket-api:1.1")
|
implementation("javax.websocket:javax.websocket-api:1.1")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
|
@ -72,7 +73,7 @@ dependencies {
|
||||||
implementation("org.modelmapper:modelmapper:2.3.2")
|
implementation("org.modelmapper:modelmapper:2.3.2")
|
||||||
|
|
||||||
api("org.springframework.boot:spring-boot-starter-test")
|
api("org.springframework.boot:spring-boot-starter-test")
|
||||||
/*api("org.springframework.security:spring-security-jwt:1.0.10.RELEASE")*/
|
api("org.springframework.security:spring-security-jwt:1.1.1.RELEASE")
|
||||||
|
|
||||||
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0")
|
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0")
|
||||||
testImplementation("io.projectreactor:reactor-test")
|
testImplementation("io.projectreactor:reactor-test")
|
||||||
|
|
|
@ -11,17 +11,41 @@
|
||||||
{
|
{
|
||||||
"name": "getProjects",
|
"name": "getProjects",
|
||||||
"request": {
|
"request": {
|
||||||
|
"auth": {
|
||||||
|
"type": "oauth2",
|
||||||
|
"oauth2": [
|
||||||
|
{
|
||||||
|
"key": "tokenType",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "accessToken",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "grant_type",
|
||||||
|
"value": "authorization_code_with_pkce",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "addTokenTo",
|
||||||
|
"value": "header",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [],
|
"header": [],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "http://localhost:8443/v1/projects",
|
"raw": "http://localhost:8443/projects",
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"host": [
|
"host": [
|
||||||
"localhost"
|
"localhost"
|
||||||
],
|
],
|
||||||
"port": "8443",
|
"port": "8443",
|
||||||
"path": [
|
"path": [
|
||||||
"v1",
|
|
||||||
"projects"
|
"projects"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -49,6 +73,87 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"response": []
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "postKeycloakToken",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "urlencoded",
|
||||||
|
"urlencoded": [
|
||||||
|
{
|
||||||
|
"key": "client_id",
|
||||||
|
"value": "c4po_local",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "username",
|
||||||
|
"value": "ttt",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "password",
|
||||||
|
"value": "Test1234!",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "grant_type",
|
||||||
|
"value": "password",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "token",
|
||||||
|
"value": "",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "client_secret",
|
||||||
|
"value": "secret",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "http://localhost:8888/auth/realms/c4po_realm_local/protocol/openid-connect/token",
|
||||||
|
"protocol": "http",
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
],
|
||||||
|
"port": "8888",
|
||||||
|
"path": [
|
||||||
|
"auth",
|
||||||
|
"realms",
|
||||||
|
"c4po_realm_local",
|
||||||
|
"protocol",
|
||||||
|
"openid-connect",
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "getASCIIDocumentation",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "http://localhost:8443/docs/SecurityC4PO.html",
|
||||||
|
"protocol": "http",
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
],
|
||||||
|
"port": "8443",
|
||||||
|
"path": [
|
||||||
|
"docs",
|
||||||
|
"SecurityC4PO.html"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -25,15 +25,15 @@ When creating requests you must not follow the examples exactly, e.g. instead of
|
||||||
|
|
||||||
=== Get projects
|
=== Get projects
|
||||||
|
|
||||||
To get projects, call the GET request /v1/projects
|
To get projects, call the GET request /projects
|
||||||
|
|
||||||
==== Request example
|
==== Request example
|
||||||
|
|
||||||
#include::{snippets}/getProjects/http-request.adoc[]
|
#include::{snippets}/getProjects/com.securityc4po.api.http-request.adoc[]
|
||||||
|
|
||||||
==== Response example
|
==== Response example
|
||||||
|
|
||||||
#include::{snippets}/getProjects/http-response.adoc[]
|
#include::{snippets}/getProjects/com.securityc4po.api.http-response.adoc[]
|
||||||
|
|
||||||
==== Response structure
|
==== Response structure
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.securityc4po.api.v1
|
package com.securityc4po.api
|
||||||
|
|
||||||
abstract class BaseEntity<T>(
|
abstract class BaseEntity<T>(
|
||||||
var data: T
|
var data: T
|
||||||
) {
|
) {
|
||||||
}
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
package com.securityc4po.api.v1
|
package com.securityc4po.api
|
||||||
|
|
||||||
typealias ResponseBody = Map<String, Any?>
|
typealias ResponseBody = Map<String, Any?>
|
|
@ -1,4 +1,4 @@
|
||||||
package com.securityc4po.api.v1
|
package com.securityc4po.api
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
import org.springframework.boot.runApplication
|
import org.springframework.boot.runApplication
|
|
@ -1,4 +1,4 @@
|
||||||
package com.securityc4po.api.v1.configuration
|
package com.securityc4po.api.configuration
|
||||||
|
|
||||||
// Constants for SpotBugs warning suppressions
|
// Constants for SpotBugs warning suppressions
|
||||||
const val NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR = "NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"
|
const val NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR = "NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"
|
|
@ -0,0 +1,47 @@
|
||||||
|
package com.securityc4po.api.configuration.security
|
||||||
|
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
|
import org.springframework.security.core.GrantedAuthority
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
|
|
||||||
|
class Appuser : UserDetails {
|
||||||
|
|
||||||
|
override fun getAuthorities(): Collection<GrantedAuthority> {
|
||||||
|
return listOf("user").stream().map {
|
||||||
|
it.toUpperCase()
|
||||||
|
}.map {
|
||||||
|
ROLE_PREFIX + it
|
||||||
|
}.map {
|
||||||
|
SimpleGrantedAuthority(it)
|
||||||
|
}.collect(Collectors.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPassword(): String {
|
||||||
|
return "n/a"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUsername(): String {
|
||||||
|
return "n/a"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isAccountNonExpired(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isAccountNonLocked(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isCredentialsNonExpired(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isEnabled(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val ROLE_PREFIX = "ROLE_"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.securityc4po.api.configuration.security
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import org.springframework.core.convert.converter.Converter
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||||
|
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 java.util.stream.Collectors
|
||||||
|
|
||||||
|
/** JWT converter that takes the roles from 'groups' claim of JWT token. */
|
||||||
|
class AppuserJwtAuthConverter(
|
||||||
|
private val appuserDetailsService: UserAccountDetailsService) : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
|
||||||
|
|
||||||
|
override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
|
||||||
|
val authorities = extractAuthorities(jwt)
|
||||||
|
return appuserDetailsService
|
||||||
|
.findByUsername(jwt.getClaimAsString("sub"))
|
||||||
|
.map { u ->
|
||||||
|
UsernamePasswordAuthenticationToken(u, "n/a", authorities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractAuthorities(jwt: Jwt): Collection<GrantedAuthority> {
|
||||||
|
return this.getScopes(jwt).stream().map { authority ->
|
||||||
|
ROLE_PREFIX + authority.toUpperCase()
|
||||||
|
}.map {
|
||||||
|
SimpleGrantedAuthority(it)
|
||||||
|
}.collect(Collectors.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getScopes(jwt: Jwt): Collection<String> {
|
||||||
|
val mapper = ObjectMapper()
|
||||||
|
val scopes = jwt.getClaims().get(GROUPS_CLAIM).toString()
|
||||||
|
if (scopes != null) {
|
||||||
|
val roleStringValue = mapper.readTree(scopes).get("roles").toString()
|
||||||
|
val roles = mapper.readValue<Collection<String>>(roleStringValue)
|
||||||
|
if (!roles.isEmpty()){
|
||||||
|
return roles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val GROUPS_CLAIM = "realm_access"
|
||||||
|
private val ROLE_PREFIX = "ROLE_"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.securityc4po.api.configuration.security
|
||||||
|
|
||||||
|
import org.modelmapper.ModelMapper
|
||||||
|
import org.modelmapper.convention.MatchingStrategies
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class ModelmapperCfg {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun modelMapper(): ModelMapper {
|
||||||
|
val modelMapper = ModelMapper()
|
||||||
|
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT)
|
||||||
|
return modelMapper
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
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<UserDetails> {
|
||||||
|
return Appuser().toMono()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.securityc4po.api.configuration.security
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.web.cors.CorsConfiguration
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
@EnableReactiveMethodSecurity
|
||||||
|
class WebSecurityConfiguration(private val userAccountDetailsService: UserAccountDetailsService) {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
|
http.cors().configurationSource {
|
||||||
|
CorsConfiguration().apply {
|
||||||
|
this.applyPermitDefaultValues()
|
||||||
|
this.addAllowedMethod(HttpMethod.DELETE)
|
||||||
|
this.addAllowedMethod(HttpMethod.PATCH)
|
||||||
|
this.addAllowedMethod(HttpMethod.POST)
|
||||||
|
this.addAllowedMethod(HttpMethod.GET)
|
||||||
|
this.addAllowedMethod(HttpMethod.PUT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.and()
|
||||||
|
.csrf()
|
||||||
|
.disable()
|
||||||
|
.authorizeExchange()
|
||||||
|
.pathMatchers(HttpMethod.GET, "/v1/projects/**").authenticated()
|
||||||
|
.pathMatchers("/actuator/**").permitAll()
|
||||||
|
.pathMatchers("/docs/SecurityC4PO.html").permitAll()
|
||||||
|
.anyExchange().authenticated()
|
||||||
|
.and()
|
||||||
|
.oauth2ResourceServer()
|
||||||
|
.jwt()
|
||||||
|
.jwtAuthenticationConverter(appuserJwtAuthenticationConverter())
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun appuserJwtAuthenticationConverter(): AppuserJwtAuthConverter {
|
||||||
|
return AppuserJwtAuthConverter(userAccountDetailsService)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.securityc4po.api.v1.extensions
|
package com.securityc4po.api.extensions
|
||||||
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.securityc4po.api.http
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import org.springframework.web.server.WebFilter
|
||||||
|
import org.springframework.web.server.WebFilterChain
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class AddResponseHeaderFilter: WebFilter {
|
||||||
|
|
||||||
|
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
|
||||||
|
val httpHeaders: HashMap<String, String> = HashMap()
|
||||||
|
httpHeaders.put("Application-Name", ApplicationHeaders.APPLICATION_NAME)
|
||||||
|
httpHeaders.put("X-Version", ApplicationHeaders.XVERSION.toString())
|
||||||
|
|
||||||
|
return chain.filter(
|
||||||
|
exchange.apply {
|
||||||
|
response.headers.setAll(httpHeaders)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.securityc4po.api.http
|
||||||
|
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
|
||||||
|
object ApplicationHeaders {
|
||||||
|
const val AUTHORIZATION = HttpHeaders.AUTHORIZATION
|
||||||
|
const val APPLICATION_NAME = "SecurityC4PO"
|
||||||
|
const val XVERSION = 1
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.securityc4po.api.http
|
||||||
|
|
||||||
|
import com.securityc4po.api.extensions.getLoggerFor
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.server.WebFilter
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class RequestLogIntercepter {
|
||||||
|
|
||||||
|
private val logger = getLoggerFor<RequestLogIntercepter>()
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun loggingFilter(): WebFilter =
|
||||||
|
WebFilter { exchange, chain ->
|
||||||
|
val request = exchange.request
|
||||||
|
if (request.headers.getFirst(ApplicationHeaders.AUTHORIZATION) == null) {
|
||||||
|
logger.warn("No Authorization header present for request: ${request.id}")
|
||||||
|
}
|
||||||
|
logger.info("Request recognized: [id: ${request.id}, method=${request.method}, path=${request.path.pathWithinApplication()}, params=[${request.queryParams}] }")
|
||||||
|
val result = chain.filter(exchange)
|
||||||
|
return@WebFilter result
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package com.securityc4po.api.v1.project
|
package com.securityc4po.api.project
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat
|
import com.fasterxml.jackson.annotation.JsonFormat
|
||||||
import com.securityc4po.api.v1.ResponseBody
|
import com.securityc4po.api.ResponseBody
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
package com.securityc4po.api.v1.project
|
package com.securityc4po.api.project
|
||||||
|
|
||||||
import com.securityc4po.api.v1.ResponseBody
|
import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
|
||||||
import com.securityc4po.api.v1.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
|
import com.securityc4po.api.extensions.getLoggerFor
|
||||||
import com.securityc4po.api.v1.extensions.getLoggerFor
|
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||||
|
import com.securityc4po.api.ResponseBody
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/v1/projects")
|
@RequestMapping("/projects")
|
||||||
@CrossOrigin(
|
@CrossOrigin(
|
||||||
origins = [],
|
origins = [],
|
||||||
allowCredentials = "false",
|
allowCredentials = "false",
|
||||||
|
@ -25,4 +26,4 @@ class ProjectController(private val projectService: ProjectService) {
|
||||||
return projectService.getProjects()
|
return projectService.getProjects()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package com.securityc4po.api.v1.project
|
package com.securityc4po.api.project
|
||||||
|
|
||||||
import com.securityc4po.api.v1.BaseEntity
|
import com.securityc4po.api.BaseEntity
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @Document(collection = "project")
|
* @Document(collection = "project")
|
||||||
|
@ -8,4 +8,4 @@ import com.securityc4po.api.v1.BaseEntity
|
||||||
*/
|
*/
|
||||||
open class ProjectEntity(
|
open class ProjectEntity(
|
||||||
data: Project
|
data: Project
|
||||||
) : BaseEntity<Project>(data)
|
) : BaseEntity<Project>(data)
|
|
@ -1,6 +1,6 @@
|
||||||
package com.securityc4po.api.v1.project
|
package com.securityc4po.api.project
|
||||||
|
|
||||||
import com.securityc4po.api.v1.extensions.getLoggerFor
|
import com.securityc4po.api.extensions.getLoggerFor
|
||||||
import org.junit.BeforeClass
|
import org.junit.BeforeClass
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
@ -25,7 +25,6 @@ class ProjectService() {
|
||||||
mapper.registerModule(JavaTimeModule())
|
mapper.registerModule(JavaTimeModule())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all [Project]s
|
* Get all [Project]s
|
||||||
*
|
*
|
||||||
|
@ -37,4 +36,4 @@ class ProjectService() {
|
||||||
/* After database integration the return should be Flux of ProjectEntity */
|
/* After database integration the return should be Flux of ProjectEntity */
|
||||||
return jsonProjectList;
|
return jsonProjectList;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.securityc4po.api.v1.user
|
package com.securityc4po.api.user
|
||||||
|
|
||||||
data class User(
|
data class User(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
@ -12,4 +12,4 @@ data class User(
|
||||||
val email: String? = null,
|
val email: String? = null,
|
||||||
|
|
||||||
val interfaceLang: String? = null
|
val interfaceLang: String? = null
|
||||||
)
|
)
|
|
@ -1,6 +1,6 @@
|
||||||
package com.securityc4po.api.v1.user
|
package com.securityc4po.api.user
|
||||||
|
|
||||||
import com.securityc4po.api.v1.BaseEntity
|
import com.securityc4po.api.BaseEntity
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @Document(collection = "user")
|
* @Document(collection = "user")
|
||||||
|
@ -8,4 +8,4 @@ import com.securityc4po.api.v1.BaseEntity
|
||||||
*/
|
*/
|
||||||
open class UserEntity(
|
open class UserEntity(
|
||||||
data: User
|
data: User
|
||||||
) : BaseEntity<User>(data)
|
) : BaseEntity<User>(data)
|
|
@ -15,4 +15,9 @@ management.endpoints.web.exposure.include=info, health, metrics
|
||||||
# spring.data.mongodb.host=localhost
|
# spring.data.mongodb.host=localhost
|
||||||
# spring.data.mongodb.port=27017
|
# spring.data.mongodb.port=27017
|
||||||
# spring.main.allow-bean-definition-overriding=true
|
# spring.main.allow-bean-definition-overriding=true
|
||||||
# spring.data.mongodb.auto-index-creation=true
|
# spring.data.mongodb.auto-index-creation=true
|
||||||
|
|
||||||
|
## IdentityProvider (Keycloak for tests) ##
|
||||||
|
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8888/auth/realms/c4po_realm_local
|
||||||
|
keycloakhost=localhost
|
||||||
|
keycloak.client.url=http://localhost:8888/
|
|
@ -1,13 +0,0 @@
|
||||||
package com.securityc4po.api.v1
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
class SecurityC4POApplicationTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun contextLoads() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package com.securityc4po.api.v1
|
package com.securityc4po.api
|
||||||
|
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
|
@ -1,8 +1,8 @@
|
||||||
package com.securityc4po.api.v1
|
package com.securityc4po.api
|
||||||
|
|
||||||
import com.securityc4po.api.v1.configuration.MESSAGE_NOT_INITIALIZED_REDUNDANT_NULLCHECK
|
import com.securityc4po.api.configuration.MESSAGE_NOT_INITIALIZED_REDUNDANT_NULLCHECK
|
||||||
import com.securityc4po.api.v1.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR
|
import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR
|
||||||
import com.securityc4po.api.v1.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
|
import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
@ -28,7 +28,7 @@ abstract class BaseDocumentationIntTest : BaseContainerizedTest() {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setupDocs(restDocumentation: RestDocumentationContextProvider) {
|
fun setupDocs(restDocumentation: RestDocumentationContextProvider) {
|
||||||
webTestClient = WebTestClient.bindToServer()
|
webTestClient = WebTestClient.bindToServer()
|
||||||
.baseUrl("http://localhost:$port")
|
.baseUrl("com.securityc4po.api.http://localhost:$port")
|
||||||
.filter(documentationConfiguration(restDocumentation))
|
.filter(documentationConfiguration(restDocumentation))
|
||||||
.responseTimeout(Duration.ofMillis(10000))
|
.responseTimeout(Duration.ofMillis(10000))
|
||||||
.build()
|
.build()
|
|
@ -1,4 +1,4 @@
|
||||||
package com.securityc4po.api.v1
|
package com.securityc4po.api
|
||||||
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
|
@ -1,8 +1,8 @@
|
||||||
package com.securityc4po.api.v1.project
|
package com.securityc4po.api.project
|
||||||
|
|
||||||
import com.github.tomakehurst.wiremock.common.Json
|
import com.github.tomakehurst.wiremock.common.Json
|
||||||
import com.securityc4po.api.v1.BaseDocumentationIntTest
|
import com.securityc4po.api.BaseDocumentationIntTest
|
||||||
import com.securityc4po.api.v1.configuration.SIC_INNER_SHOULD_BE_STATIC
|
import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
|
@ -1,9 +1,9 @@
|
||||||
package com.securityc4po.api.v1.project
|
package com.securityc4po.api.project
|
||||||
|
|
||||||
import com.github.tomakehurst.wiremock.common.Json
|
import com.github.tomakehurst.wiremock.common.Json
|
||||||
import com.securityc4po.api.v1.BaseIntTest
|
import com.securityc4po.api.BaseIntTest
|
||||||
import com.securityc4po.api.v1.configuration.SIC_INNER_SHOULD_BE_STATIC
|
import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
|
||||||
import com.securityc4po.api.v1.configuration.URF_UNREAD_FIELD
|
import com.securityc4po.api.configuration.URF_UNREAD_FIELD
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
|
@ -1,7 +1,7 @@
|
||||||
package com.securityc4po.api.v1.project
|
package com.securityc4po.api.project
|
||||||
|
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
import com.securityc4po.api.v1.configuration.SIC_INNER_SHOULD_BE_STATIC
|
import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
|
@ -0,0 +1,16 @@
|
||||||
|
version: '3.1'
|
||||||
|
|
||||||
|
services:
|
||||||
|
c4po-api:
|
||||||
|
build: '../../security-c4po-api'
|
||||||
|
image: security-c4po-api:latest
|
||||||
|
container_name: security-c4po-api
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "1G"
|
||||||
|
ports:
|
||||||
|
- '8443:8443'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
c4po:
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,9 @@
|
||||||
|
# cfg for local keycloak
|
||||||
|
DB_VENDOR=postgres
|
||||||
|
DB_ADDR=keycloak-postgres
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=c4po_kc_local
|
||||||
|
DB_PASSWORD=Test1234!
|
||||||
|
KEYCLOAK_USER=admin
|
||||||
|
KEYCLOAK_PASSWORD=admin
|
||||||
|
KEYCLOAK_IMPORT=/tmp/c4po_realm_export.json
|
|
@ -0,0 +1,4 @@
|
||||||
|
# database.env
|
||||||
|
POSTGRES_USER=c4po_kc_local
|
||||||
|
POSTGRES_PASSWORD=Test1234!
|
||||||
|
POSTGRES_DB=keycloak
|
|
@ -0,0 +1,16 @@
|
||||||
|
version: '3.1'
|
||||||
|
|
||||||
|
services:
|
||||||
|
c4po-angular:
|
||||||
|
build: '../../security-c4po-angular'
|
||||||
|
image: security-c4po-angular:latest
|
||||||
|
container_name: security-c4po-angular
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "1G"
|
||||||
|
ports:
|
||||||
|
- '4200:4200'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
c4po:
|
|
@ -0,0 +1,24 @@
|
||||||
|
version: '3.1'
|
||||||
|
|
||||||
|
services:
|
||||||
|
c4po-keycloak:
|
||||||
|
container_name: security-c4po-keycloak
|
||||||
|
depends_on:
|
||||||
|
- keycloak-postgres
|
||||||
|
image: jboss/keycloak:11.0.3
|
||||||
|
volumes:
|
||||||
|
- ../cfg/c4po_realm_export.json:/tmp/c4po_realm_export.json
|
||||||
|
ports:
|
||||||
|
- 8888:8080
|
||||||
|
- 9990:9990
|
||||||
|
env_file:
|
||||||
|
- ../cfg/keycloak.env
|
||||||
|
keycloak-postgres:
|
||||||
|
container_name: security-c4po-postgres-keycloak
|
||||||
|
image: postgres:latest
|
||||||
|
env_file:
|
||||||
|
- ../cfg/keycloakdb.env
|
||||||
|
ports:
|
||||||
|
- 5433:5432
|
||||||
|
volumes:
|
||||||
|
- ../volumes/keycloak/data:/var/lib/postgresql/data
|
Loading…
Reference in New Issue