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
|
||||
baseDir=$(pwd)"/"
|
||||
echo"
|
||||
docker_reg="c4po.io"
|
||||
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 @@ ______| |______ |_____ |_____| | \_ __|__ | | _/_/_/ _/
|
|||
_/ _/_/_/_/ _/_/_/ _/ _/
|
||||
_/ _/ _/ _/ _/
|
||||
_/_/_/ _/ _/ _/_/
|
||||
"
|
||||
#docker-compose up --build
|
||||
docker-compose up
|
||||
\n"
|
||||
|
||||
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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
@ -8782,6 +8787,11 @@
|
|||
"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": {
|
||||
"version": "1.4.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
|
|
|
@ -35,6 +35,9 @@
|
|||
"@ngxs/store": "^3.7.0",
|
||||
"eva-icons": "^1.1.3",
|
||||
"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-timezone": "latest",
|
||||
"ngx-moment": "^5.0.0",
|
||||
|
|
|
@ -2,10 +2,8 @@ import { NgModule } from '@angular/core';
|
|||
import { Routes, RouterModule } from '@angular/router';
|
||||
import {HomeComponent} from './home/home.component';
|
||||
import {AuthGuardService} from '../shared/guards/auth-guard.service';
|
||||
import {LoginGuardService} from '../shared/guards/login-guard.service';
|
||||
|
||||
export const START_PAGE = 'home';
|
||||
export const FALLBACK_PAGE = 'home';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -18,11 +16,12 @@ const routes: Routes = [
|
|||
loadChildren: () => import('./dashboard').then(mod => mod.DashboardModule),
|
||||
canActivate: [AuthGuardService]
|
||||
},
|
||||
{
|
||||
// ToDo: Exchange default Keycloak login with self made login
|
||||
/*{
|
||||
path: 'login',
|
||||
loadChildren: () => import('./login').then(mod => mod.LoginModule),
|
||||
canActivate: [LoginGuardService]
|
||||
},
|
||||
},*/
|
||||
{path: '**', redirectTo: START_PAGE},
|
||||
{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 {NgxsModule} from '@ngxs/store';
|
||||
import {HeaderModule} from './header/header.module';
|
||||
import {KeycloakService} from 'keycloak-angular';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
|
@ -32,6 +33,9 @@ describe('AppComponent', () => {
|
|||
NgxsModule.forRoot([SessionState]),
|
||||
HttpClientTestingModule
|
||||
],
|
||||
providers: [
|
||||
KeycloakService
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {APP_INITIALIZER, NgModule} from '@angular/core';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {AppRoutingModule} from './app-routing.module';
|
||||
import {AppComponent} from './app.component';
|
||||
|
@ -23,6 +23,8 @@ 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';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -55,9 +57,19 @@ import {HomeModule} from './home/home.module';
|
|||
],
|
||||
providers: [
|
||||
HttpClient,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initializer,
|
||||
multi: true,
|
||||
deps: [KeycloakService]
|
||||
},
|
||||
KeycloakService,
|
||||
httpInterceptorProviders,
|
||||
NotificationService
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
bootstrap: [
|
||||
AppComponent
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
constructor(library: FaIconLibrary, faConfig: FaConfig) {
|
||||
|
@ -65,3 +77,30 @@ export class AppModule {
|
|||
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 () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DashboardComponent ]
|
||||
declarations: [
|
||||
DashboardComponent
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||
import { HomeComponent } from './home.component';
|
||||
import {NbButtonModule, NbCardModule} from '@nebular/theme';
|
||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||
import {KeycloakService} from 'keycloak-angular';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
|
@ -17,6 +18,9 @@ describe('HomeComponent', () => {
|
|||
HttpClientTestingModule,
|
||||
NbCardModule,
|
||||
NbButtonModule
|
||||
],
|
||||
providers : [
|
||||
KeycloakService
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
|
|
@ -23,6 +23,7 @@ import {CommonModule} from '@angular/common';
|
|||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {NotificationService} from '../../shared/services/notification.service';
|
||||
import {NotificationServiceMock} from '../../shared/services/notification.service.mock';
|
||||
import {KeycloakService} from 'keycloak-angular';
|
||||
|
||||
const DESIRED_STORE_STATE_SESSION: SessionStateModel = {
|
||||
userAccount: {
|
||||
|
@ -68,6 +69,7 @@ describe('LoginComponent', () => {
|
|||
LoginComponent
|
||||
],
|
||||
providers: [
|
||||
KeycloakService,
|
||||
{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 {HttpClient} from '@angular/common/http';
|
||||
import {FieldStatus} from '../../shared/models/form-field-status.model';
|
||||
import {KeycloakService} from 'keycloak-angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
// ToDo: Exchange default Keycloak login with self made login
|
||||
export class LoginComponent implements OnInit, OnDestroy {
|
||||
readonly MIN_LENGTH: number = 2;
|
||||
readonly SECURITYC4PO_TITLE = GlobalTitlesVariables.SECURITYC4PO_TITLE;
|
||||
|
@ -41,7 +43,8 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||
private router: Router,
|
||||
private store: Store,
|
||||
private readonly httpClient: HttpClient,
|
||||
private notificationService: NotificationService) {
|
||||
private notificationService: NotificationService,
|
||||
protected keycloakService: KeycloakService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -65,8 +68,9 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||
login(): void {
|
||||
const username = this.loginUsernameCtrl.value;
|
||||
const password = this.loginPasswordCtrl.value;
|
||||
if (username === DefaultUser.username
|
||||
&& password === DefaultUser.password) {
|
||||
// ToDo: Should be handled in Guards
|
||||
this.keycloakService.login({});
|
||||
if (true) {
|
||||
// ToDo: Should be handled in Guards
|
||||
this.store.dispatch(new UpdateIsAuthenticated(true));
|
||||
this.store.dispatch(new UpdateUser(this.user, true));
|
||||
|
@ -120,10 +124,3 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||
export interface Version {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export enum DefaultUser {
|
||||
username = 'ttt',
|
||||
password = 'Test1234!'
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
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 = {
|
||||
stage: 'n/a',
|
||||
production: false,
|
||||
|
||||
// keycloak
|
||||
keycloakURL: 'http://localhost:8888/auth',
|
||||
keycloakrealm: 'c4po_realm_local',
|
||||
keycloakclientId: 'c4po_local',
|
||||
|
||||
// backend service
|
||||
apiEndpoint: 'http://localhost:8443',
|
||||
};
|
||||
|
||||
|
|
|
@ -4,45 +4,42 @@ 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'
|
||||
})
|
||||
export class AuthGuardService implements CanActivate {
|
||||
export class AuthGuardService extends KeycloakAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
public readonly router: Router,
|
||||
protected keycloakService: KeycloakService,
|
||||
private readonly store: Store) {
|
||||
super(router, keycloakService);
|
||||
}
|
||||
|
||||
canActivate(
|
||||
next: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot): Observable<boolean> {
|
||||
return this.isAuthenticated()
|
||||
.pipe(
|
||||
map((canAccess: boolean) => {
|
||||
if (canAccess) {
|
||||
return canAccess;
|
||||
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 {
|
||||
this.router.navigate(['/login']);
|
||||
return false;
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
private isAuthenticated(): Observable<boolean> {
|
||||
// ToDo: Should check from Authentication Provider
|
||||
return of(this.store.selectSnapshot(SessionState.isAuthenticated))
|
||||
.pipe(
|
||||
map((isLoggedIn: boolean) => {
|
||||
return isLoggedIn;
|
||||
}),
|
||||
catchError(() => {
|
||||
return of(false);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ 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;
|
||||
|
@ -27,6 +28,7 @@ describe('LoginGuardService', () => {
|
|||
NgxsModule.forRoot([SessionState])
|
||||
],
|
||||
providers: [
|
||||
KeycloakService
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(LoginGuardService);
|
||||
|
|
|
@ -4,18 +4,45 @@ import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '
|
|||
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 implements CanActivate {
|
||||
|
||||
export class LoginGuardService extends KeycloakAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private store: Store) {
|
||||
public readonly router: Router,
|
||||
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(
|
||||
tap((canAccess: boolean) => {
|
||||
if (canAccess) {
|
||||
|
@ -27,9 +54,9 @@ export class LoginGuardService implements CanActivate {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
/!**
|
||||
* @return state of authentication
|
||||
*/
|
||||
*!/
|
||||
private isAuthenticated(): Observable<boolean> {
|
||||
// ToDo: Should check from Authentication Provider
|
||||
return of(this.store.selectSnapshot(SessionState.isAuthenticated))
|
||||
|
@ -41,6 +68,6 @@ export class LoginGuardService implements CanActivate {
|
|||
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';
|
||||
|
||||
export class User {
|
||||
id: string;
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
mailAddress: string;
|
||||
interfaceLang: string;
|
||||
id?: string;
|
||||
username?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
mailAddress?: string;
|
||||
interfaceLang?: string;
|
||||
|
||||
constructor(username?: string,
|
||||
firstName?: string,
|
||||
|
@ -17,7 +17,15 @@ export class User {
|
|||
this.username = username;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
if (email) {
|
||||
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 {NgxsModule} from '@ngxs/store';
|
||||
import {SessionState} from '../stores/session-state/session-state';
|
||||
import {KeycloakService} from 'keycloak-angular';
|
||||
|
||||
describe('NotificationService', () => {
|
||||
let toastrServiceStub: Partial<NbToastrService>;
|
||||
|
@ -45,6 +46,7 @@ describe('NotificationService', () => {
|
|||
],
|
||||
providers: [
|
||||
NotificationService,
|
||||
KeycloakService,
|
||||
{provide: NbToastrService, useValue: toastrServiceStub},
|
||||
{provide: TranslateService, useValue: translateServiceStub}]
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import { TestBed } from '@angular/core/testing';
|
|||
import { ProjectService } from './project.service';
|
||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {KeycloakService} from 'keycloak-angular';
|
||||
|
||||
describe('ProjectService', () => {
|
||||
let service: ProjectService;
|
||||
|
@ -13,7 +14,9 @@ describe('ProjectService', () => {
|
|||
HttpClientTestingModule,
|
||||
BrowserAnimationsModule,
|
||||
],
|
||||
providers: []
|
||||
providers: [
|
||||
KeycloakService
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(ProjectService);
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import {Observable} from 'rxjs';
|
|||
})
|
||||
export class ProjectService {
|
||||
|
||||
private apiBaseURL = `${environment.apiEndpoint}/v1/projects`;
|
||||
private apiBaseURL = `${environment.apiEndpoint}/projects`;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {HttpLoaderFactory} from '../../app/common-app.module';
|
|||
import {HttpClient} from '@angular/common/http';
|
||||
import {NgxsModule} from '@ngxs/store';
|
||||
import {SessionState} from '../stores/session-state/session-state';
|
||||
import {KeycloakService} from 'keycloak-angular';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
@ -26,7 +27,9 @@ describe('UserService', () => {
|
|||
}
|
||||
}),
|
||||
],
|
||||
providers: []
|
||||
providers: [
|
||||
KeycloakService
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(UserService);
|
||||
});
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||
import {User} from '../models/user.model';
|
||||
import {Observable} from 'rxjs';
|
||||
import {from, Observable, Subscriber} from 'rxjs';
|
||||
import {Store} from '@ngxs/store';
|
||||
import {SessionState} from '../stores/session-state/session-state';
|
||||
import {KeycloakService} from 'keycloak-angular';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -11,6 +12,7 @@ import {SessionState} from '../stores/session-state/session-state';
|
|||
export class UserService {
|
||||
|
||||
constructor(private http: HttpClient,
|
||||
private keycloakService: KeycloakService,
|
||||
private store: Store) {
|
||||
}
|
||||
|
||||
|
@ -20,7 +22,33 @@ export class UserService {
|
|||
});
|
||||
}
|
||||
|
||||
getCurrentAuthenticatedUser(): Observable<User> {
|
||||
return this.store.select(SessionState.userAccount);
|
||||
private createHttpOptions(): Observable<any> {
|
||||
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';
|
||||
}
|
||||
|
||||
export class FetchUser {
|
||||
static readonly type = '[Session] FetchUser';
|
||||
}
|
||||
|
||||
export class 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 {User} from '../../models/user.model';
|
||||
import {InitSession, UpdateUser} from './session-state.actions';
|
||||
import {KeycloakService} from 'keycloak-angular';
|
||||
|
||||
const INITIAL_STORE_STATE_SESSION: SessionStateModel = {
|
||||
userAccount: {
|
||||
|
@ -40,7 +41,9 @@ describe('SessionState', () => {
|
|||
}),
|
||||
NgxsModule.forRoot([SessionState]),
|
||||
],
|
||||
providers: []
|
||||
providers: [
|
||||
KeycloakService
|
||||
]
|
||||
});
|
||||
store = TestBed.inject(Store);
|
||||
store.reset({
|
||||
|
|
|
@ -2,9 +2,10 @@ import {User} from '../../models/user.model';
|
|||
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
|
||||
import {Action, Selector, State, StateContext} from '@ngxs/store';
|
||||
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 moment from 'moment';
|
||||
import {UserService} from '../../services/user.service';
|
||||
|
||||
export interface SessionStateModel {
|
||||
userAccount: User;
|
||||
|
@ -24,6 +25,7 @@ export const SESSION_STORAGE_KEY_USER = 'user';
|
|||
@Injectable()
|
||||
export class SessionState {
|
||||
constructor(@Inject(LOCALE_ID) private readonly localeId: string,
|
||||
private readonly userService: UserService,
|
||||
private readonly translateService: TranslateService) {
|
||||
}
|
||||
|
||||
|
@ -51,6 +53,17 @@ export class SessionState {
|
|||
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)
|
||||
updateUser(ctx: StateContext<SessionStateModel>, {user, force}: UpdateUser): void {
|
||||
const state = ctx.getState();
|
||||
|
|
|
@ -61,6 +61,7 @@ dependencies {
|
|||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:2.11.3")
|
||||
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions:1.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-actuator")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
|
@ -72,7 +73,7 @@ dependencies {
|
|||
implementation("org.modelmapper:modelmapper:2.3.2")
|
||||
|
||||
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("io.projectreactor:reactor-test")
|
||||
|
|
|
@ -11,17 +11,41 @@
|
|||
{
|
||||
"name": "getProjects",
|
||||
"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",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "http://localhost:8443/v1/projects",
|
||||
"raw": "http://localhost:8443/projects",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8443",
|
||||
"path": [
|
||||
"v1",
|
||||
"projects"
|
||||
]
|
||||
}
|
||||
|
@ -49,6 +73,87 @@
|
|||
}
|
||||
},
|
||||
"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
|
||||
|
||||
To get projects, call the GET request /v1/projects
|
||||
To get projects, call the GET request /projects
|
||||
|
||||
==== Request example
|
||||
|
||||
#include::{snippets}/getProjects/http-request.adoc[]
|
||||
#include::{snippets}/getProjects/com.securityc4po.api.http-request.adoc[]
|
||||
|
||||
==== Response example
|
||||
|
||||
#include::{snippets}/getProjects/http-response.adoc[]
|
||||
#include::{snippets}/getProjects/com.securityc4po.api.http-response.adoc[]
|
||||
|
||||
==== Response structure
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package com.securityc4po.api.v1
|
||||
package com.securityc4po.api
|
||||
|
||||
abstract class BaseEntity<T>(
|
||||
var data: T
|
|
@ -1,3 +1,3 @@
|
|||
package com.securityc4po.api.v1
|
||||
package com.securityc4po.api
|
||||
|
||||
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.runApplication
|
|
@ -1,4 +1,4 @@
|
|||
package com.securityc4po.api.v1.configuration
|
||||
package com.securityc4po.api.configuration
|
||||
|
||||
// Constants for SpotBugs warning suppressions
|
||||
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
|
||||
|
|
@ -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.securityc4po.api.v1.ResponseBody
|
||||
import com.securityc4po.api.ResponseBody
|
||||
import java.time.Instant
|
||||
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.v1.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
|
||||
import com.securityc4po.api.v1.extensions.getLoggerFor
|
||||
import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
|
||||
import com.securityc4po.api.extensions.getLoggerFor
|
||||
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.web.bind.annotation.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1/projects")
|
||||
@RequestMapping("/projects")
|
||||
@CrossOrigin(
|
||||
origins = [],
|
||||
allowCredentials = "false",
|
|
@ -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")
|
|
@ -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.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
|
@ -25,7 +25,6 @@ class ProjectService() {
|
|||
mapper.registerModule(JavaTimeModule())
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all [Project]s
|
||||
*
|
|
@ -1,4 +1,4 @@
|
|||
package com.securityc4po.api.v1.user
|
||||
package com.securityc4po.api.user
|
||||
|
||||
data class User(
|
||||
val id: String,
|
|
@ -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")
|
|
@ -16,3 +16,8 @@ management.endpoints.web.exposure.include=info, health, metrics
|
|||
# spring.data.mongodb.port=27017
|
||||
# spring.main.allow-bean-definition-overriding=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.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.v1.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.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.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
|
@ -28,7 +28,7 @@ abstract class BaseDocumentationIntTest : BaseContainerizedTest() {
|
|||
@BeforeEach
|
||||
fun setupDocs(restDocumentation: RestDocumentationContextProvider) {
|
||||
webTestClient = WebTestClient.bindToServer()
|
||||
.baseUrl("http://localhost:$port")
|
||||
.baseUrl("com.securityc4po.api.http://localhost:$port")
|
||||
.filter(documentationConfiguration(restDocumentation))
|
||||
.responseTimeout(Duration.ofMillis(10000))
|
||||
.build()
|
|
@ -1,4 +1,4 @@
|
|||
package com.securityc4po.api.v1
|
||||
package com.securityc4po.api
|
||||
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
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.securityc4po.api.v1.BaseDocumentationIntTest
|
||||
import com.securityc4po.api.v1.configuration.SIC_INNER_SHOULD_BE_STATIC
|
||||
import com.securityc4po.api.BaseDocumentationIntTest
|
||||
import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
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.securityc4po.api.v1.BaseIntTest
|
||||
import com.securityc4po.api.v1.configuration.SIC_INNER_SHOULD_BE_STATIC
|
||||
import com.securityc4po.api.v1.configuration.URF_UNREAD_FIELD
|
||||
import com.securityc4po.api.BaseIntTest
|
||||
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 org.junit.jupiter.api.BeforeEach
|
||||
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.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 org.junit.jupiter.api.Nested
|
||||
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