From e046c74da00b76009935015a253a4a5bc2ce066e Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Tue, 20 Oct 2020 10:59:23 +0300 Subject: [PATCH] added role handling --- apps/red-ui/src/app/app.module.ts | 130 ++++++++++-------- apps/red-ui/src/app/auth/auth.guard.ts | 3 + apps/red-ui/src/app/auth/red-role.guard.ts | 30 ++++ .../auth-error/auth-error.component.html | 4 + .../auth-error/auth-error.component.scss | 3 + .../auth-error/auth-error.component.ts | 19 +++ apps/red-ui/src/app/user/user.service.ts | 2 +- .../src/app/utils/composite-route.guard.ts | 69 +++++----- apps/red-ui/src/assets/i18n/en.json | 8 ++ 9 files changed, 172 insertions(+), 96 deletions(-) create mode 100644 apps/red-ui/src/app/auth/red-role.guard.ts create mode 100644 apps/red-ui/src/app/screens/auth-error/auth-error.component.html create mode 100644 apps/red-ui/src/app/screens/auth-error/auth-error.component.scss create mode 100644 apps/red-ui/src/app/screens/auth-error/auth-error.component.ts diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index b8fe0354e..a99349460 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -1,58 +1,60 @@ -import { BrowserModule } from '@angular/platform-browser'; -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {APP_INITIALIZER, NgModule} from '@angular/core'; -import { AppComponent } from './app.component'; -import {ActivatedRoute, ActivatedRouteSnapshot, Router, RouterModule} from '@angular/router'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ReactiveFormsModule, FormsModule } from '@angular/forms'; -import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; -import { BaseScreenComponent } from './screens/base-screen/base-screen.component'; -import { ProjectListingScreenComponent } from './screens/project-listing-screen/project-listing-screen.component'; -import { ProjectOverviewScreenComponent } from './screens/project-overview-screen/project-overview-screen.component'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { ApiModule } from '@redaction/red-ui-http'; -import { ApiPathInterceptorService } from './interceptor/api-path-interceptor.service'; -import { MatButtonModule } from '@angular/material/button'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { TranslateHttpLoader } from '@ngx-translate/http-loader'; -import { MatMenuModule } from '@angular/material/menu'; -import { languageInitializer } from './i18n/language.initializer'; -import { LanguageService } from './i18n/language.service'; -import { MatIconModule } from '@angular/material/icon'; -import { IconsModule } from './icons/icons.module'; -import { AddEditProjectDialogComponent } from './screens/project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { ConfirmationDialogComponent } from './common/confirmation-dialog/confirmation-dialog.component'; -import { FilePreviewScreenComponent } from './screens/file/file-preview-screen/file-preview-screen.component'; -import { PdfViewerComponent } from './screens/file/pdf-viewer/pdf-viewer.component'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatButtonToggleModule } from '@angular/material/button-toggle'; -import { NgpSortModule } from 'ngp-sort-pipe'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatSelectModule } from '@angular/material/select'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { FileDetailsDialogComponent } from './screens/file/file-preview-screen/file-details-dialog/file-details-dialog.component'; -import { ToastrModule } from 'ngx-toastr'; -import { ServiceWorkerModule } from '@angular/service-worker'; -import { environment } from '../environments/environment'; -import { ProjectDetailsDialogComponent } from './screens/project-overview-screen/project-details-dialog/project-details-dialog.component'; -import { AuthModule } from './auth/auth.module'; -import { FileUploadModule } from './upload/file-upload.module'; -import { FullPageLoadingIndicatorComponent } from './utils/full-page-loading-indicator/full-page-loading-indicator.component'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { InitialsAvatarComponent } from './common/initials-avatar/initials-avatar.component'; -import { StatusBarComponent } from './components/status-bar/status-bar.component'; -import { LogoComponent } from './logo/logo.component'; -import { CompositeRouteGuard } from './utils/composite-route.guard'; -import { AppStateGuard } from './state/app-state.guard'; -import { SimpleDoughnutChartComponent } from './components/simple-doughnut-chart/simple-doughnut-chart.component'; -import { ManualRedactionDialogComponent } from './screens/file/manual-redaction-dialog/manual-redaction-dialog.component'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { AnnotationIconComponent } from './components/annotation-icon/annotation-icon.component'; -import { AuthGuard } from "./auth/auth.guard"; +import {AppComponent} from './app.component'; +import {ActivatedRoute, Router, RouterModule} from '@angular/router'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from '@angular/common/http'; +import {BaseScreenComponent} from './screens/base-screen/base-screen.component'; +import {ProjectListingScreenComponent} from './screens/project-listing-screen/project-listing-screen.component'; +import {ProjectOverviewScreenComponent} from './screens/project-overview-screen/project-overview-screen.component'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {ApiModule} from '@redaction/red-ui-http'; +import {ApiPathInterceptorService} from './interceptor/api-path-interceptor.service'; +import {MatButtonModule} from '@angular/material/button'; +import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import {TranslateHttpLoader} from '@ngx-translate/http-loader'; +import {MatMenuModule} from '@angular/material/menu'; +import {languageInitializer} from './i18n/language.initializer'; +import {LanguageService} from './i18n/language.service'; +import {MatIconModule} from '@angular/material/icon'; +import {IconsModule} from './icons/icons.module'; +import {AddEditProjectDialogComponent} from './screens/project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatSnackBarModule} from '@angular/material/snack-bar'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {ConfirmationDialogComponent} from './common/confirmation-dialog/confirmation-dialog.component'; +import {FilePreviewScreenComponent} from './screens/file/file-preview-screen/file-preview-screen.component'; +import {PdfViewerComponent} from './screens/file/pdf-viewer/pdf-viewer.component'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {NgpSortModule} from 'ngp-sort-pipe'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatSelectModule} from '@angular/material/select'; +import {MatSidenavModule} from '@angular/material/sidenav'; +import {FileDetailsDialogComponent} from './screens/file/file-preview-screen/file-details-dialog/file-details-dialog.component'; +import {ToastrModule} from 'ngx-toastr'; +import {ServiceWorkerModule} from '@angular/service-worker'; +import {environment} from '../environments/environment'; +import {ProjectDetailsDialogComponent} from './screens/project-overview-screen/project-details-dialog/project-details-dialog.component'; +import {AuthModule} from './auth/auth.module'; +import {FileUploadModule} from './upload/file-upload.module'; +import {FullPageLoadingIndicatorComponent} from './utils/full-page-loading-indicator/full-page-loading-indicator.component'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {InitialsAvatarComponent} from './common/initials-avatar/initials-avatar.component'; +import {StatusBarComponent} from './components/status-bar/status-bar.component'; +import {LogoComponent} from './logo/logo.component'; +import {CompositeRouteGuard} from './utils/composite-route.guard'; +import {AppStateGuard} from './state/app-state.guard'; +import {SimpleDoughnutChartComponent} from './components/simple-doughnut-chart/simple-doughnut-chart.component'; +import {ManualRedactionDialogComponent} from './screens/file/manual-redaction-dialog/manual-redaction-dialog.component'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatSlideToggleModule} from '@angular/material/slide-toggle'; +import {AnnotationIconComponent} from './components/annotation-icon/annotation-icon.component'; +import {AuthGuard} from "./auth/auth.guard"; +import {AuthErrorComponent} from './screens/auth-error/auth-error.component'; +import {RedRoleGuard} from "./auth/red-role.guard"; export function HttpLoaderFactory(httpClient: HttpClient) { return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json'); @@ -77,6 +79,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) { SimpleDoughnutChartComponent, ManualRedactionDialogComponent, AnnotationIconComponent, + AuthErrorComponent, ], imports: [ BrowserModule, @@ -101,6 +104,11 @@ export function HttpLoaderFactory(httpClient: HttpClient) { redirectTo: 'ui/projects', pathMatch: 'full' }, + { + path: 'auth-error', + component: AuthErrorComponent, + canActivate: [AuthGuard] + }, { path: 'ui', component: BaseScreenComponent, @@ -110,7 +118,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) { component: ProjectListingScreenComponent, canActivate: [CompositeRouteGuard], data: { - routeGuards: [AuthGuard, AppStateGuard] + routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard] } }, { @@ -118,7 +126,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) { component: ProjectOverviewScreenComponent, canActivate: [CompositeRouteGuard], data: { - routeGuards: [AuthGuard, AppStateGuard] + routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard] } }, { @@ -126,7 +134,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) { component: FilePreviewScreenComponent, canActivate: [CompositeRouteGuard], data: { - routeGuards: [AuthGuard, AppStateGuard] + routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard] } } ] @@ -148,7 +156,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) { MatSelectModule, MatSidenavModule, FileUploadModule, - ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }), + ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}), MatProgressSpinnerModule, MatCheckboxModule ], @@ -166,9 +174,9 @@ export function HttpLoaderFactory(httpClient: HttpClient) { }) export class AppModule { - constructor(private router: Router,private route: ActivatedRoute) { - route.queryParamMap.subscribe(queryParams=>{ - if(queryParams.has('code') || queryParams.has('state') || queryParams.has('session_state')) { + constructor(private router: Router, private route: ActivatedRoute) { + route.queryParamMap.subscribe(queryParams => { + if (queryParams.has('code') || queryParams.has('state') || queryParams.has('session_state')) { this.router.navigate([], { queryParams: { 'state': null, diff --git a/apps/red-ui/src/app/auth/auth.guard.ts b/apps/red-ui/src/app/auth/auth.guard.ts index b01f1d182..3e5d8f4ea 100644 --- a/apps/red-ui/src/app/auth/auth.guard.ts +++ b/apps/red-ui/src/app/auth/auth.guard.ts @@ -2,6 +2,7 @@ import {Injectable} from "@angular/core"; import {ActivatedRouteSnapshot, Router, RouterStateSnapshot} from "@angular/router"; import {KeycloakAuthGuard, KeycloakService} from "keycloak-angular"; import {UserService} from "../user/user.service"; +import {AppLoadStateService} from "../utils/app-load-state.service"; @Injectable({ providedIn: 'root', @@ -10,6 +11,7 @@ export class AuthGuard extends KeycloakAuthGuard { constructor( protected readonly _router: Router, protected readonly _keycloak: KeycloakService, + private readonly _appLoadStateService: AppLoadStateService, private readonly _userService: UserService ) { super(_router, _keycloak); @@ -25,6 +27,7 @@ export class AuthGuard extends KeycloakAuthGuard { await this._userService.loadCurrentUser(); + return true; } } diff --git a/apps/red-ui/src/app/auth/red-role.guard.ts b/apps/red-ui/src/app/auth/red-role.guard.ts new file mode 100644 index 000000000..6576473a8 --- /dev/null +++ b/apps/red-ui/src/app/auth/red-role.guard.ts @@ -0,0 +1,30 @@ +import {Injectable} from "@angular/core"; +import {ActivatedRouteSnapshot, Router, RouterStateSnapshot} from "@angular/router"; +import {KeycloakAuthGuard, KeycloakService} from "keycloak-angular"; +import {UserService} from "../user/user.service"; +import {AppLoadStateService} from "../utils/app-load-state.service"; + +@Injectable({ + providedIn: 'root', +}) +export class RedRoleGuard extends KeycloakAuthGuard { + constructor( + protected readonly _router: Router, + protected readonly _keycloak: KeycloakService, + private readonly _appLoadStateService: AppLoadStateService, + private readonly _userService: UserService + ) { + super(_router, _keycloak); + } + + public async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + + if (!this._userService.user.hasAnyREDRoles) { + this._router.navigate(['/auth-error']); + this._appLoadStateService.pushLoadingEvent(false); + return false; + } + + return true; + } +} diff --git a/apps/red-ui/src/app/screens/auth-error/auth-error.component.html b/apps/red-ui/src/app/screens/auth-error/auth-error.component.html new file mode 100644 index 000000000..d0dd895e7 --- /dev/null +++ b/apps/red-ui/src/app/screens/auth-error/auth-error.component.html @@ -0,0 +1,4 @@ +
+

+ +
diff --git a/apps/red-ui/src/app/screens/auth-error/auth-error.component.scss b/apps/red-ui/src/app/screens/auth-error/auth-error.component.scss new file mode 100644 index 000000000..500a55a0d --- /dev/null +++ b/apps/red-ui/src/app/screens/auth-error/auth-error.component.scss @@ -0,0 +1,3 @@ +section { + padding: 24px; +} diff --git a/apps/red-ui/src/app/screens/auth-error/auth-error.component.ts b/apps/red-ui/src/app/screens/auth-error/auth-error.component.ts new file mode 100644 index 000000000..b523af973 --- /dev/null +++ b/apps/red-ui/src/app/screens/auth-error/auth-error.component.ts @@ -0,0 +1,19 @@ +import { Component, OnInit } from '@angular/core'; +import {UserService} from "../../user/user.service"; + +@Component({ + selector: 'redaction-auth-error', + templateUrl: './auth-error.component.html', + styleUrls: ['./auth-error.component.scss'] +}) +export class AuthErrorComponent implements OnInit { + + constructor(private readonly _userService: UserService) { } + + ngOnInit(): void { + } + + logout() { + this._userService.logout(); + } +} diff --git a/apps/red-ui/src/app/user/user.service.ts b/apps/red-ui/src/app/user/user.service.ts index f6f99dcde..d4e9e59fa 100644 --- a/apps/red-ui/src/app/user/user.service.ts +++ b/apps/red-ui/src/app/user/user.service.ts @@ -44,7 +44,7 @@ export class UserService { } logout() { - this._keycloakService.logout(); + this._keycloakService.logout(window.location.origin); } async loadCurrentUser() { diff --git a/apps/red-ui/src/app/utils/composite-route.guard.ts b/apps/red-ui/src/app/utils/composite-route.guard.ts index 6a62a009d..f37ddefcd 100644 --- a/apps/red-ui/src/app/utils/composite-route.guard.ts +++ b/apps/red-ui/src/app/utils/composite-route.guard.ts @@ -1,7 +1,7 @@ -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; -import { Injectable, Injector } from '@angular/core'; -import { Observable, of } from 'rxjs'; -import { catchError, mergeMap, tap } from 'rxjs/operators'; +import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; +import {Injectable, Injector} from '@angular/core'; +import {Observable, of} from 'rxjs'; +import {catchError, mergeMap, tap} from 'rxjs/operators'; import {AppLoadStateService} from "./app-load-state.service"; @Injectable({ @@ -10,40 +10,41 @@ import {AppLoadStateService} from "./app-load-state.service"; export class CompositeRouteGuard implements CanActivate { - constructor(protected router: Router, protected injector: Injector, private appLoadStateService: AppLoadStateService) {} + constructor(protected readonly _router: Router, protected readonly _injector: Injector, private readonly _appLoadStateService: AppLoadStateService) { + } - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - this.appLoadStateService.pushLoadingEvent(true); - let compositeCanActivateObservable: Observable = of(true); + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + this._appLoadStateService.pushLoadingEvent(true); + let compositeCanActivateObservable: Observable = of(true); - const routeGuards = route.data.routeGuards; - - if (routeGuards) { - for (let i = 0; i < routeGuards.length; i++) { - const routeGuard = this.injector.get(routeGuards[i]); - const canActivateObservable: Observable = routeGuard.canActivate(route, state); - compositeCanActivateObservable = compositeCanActivateObservable.pipe( - mergeMap(bool => { - if (!bool) { - return of(false); - } else { - return canActivateObservable; - } - }) - ); - } - } + const routeGuards = route.data.routeGuards; + if (routeGuards) { + for (let i = 0; i < routeGuards.length; i++) { + const routeGuard = this._injector.get(routeGuards[i]); + const canActivateObservable: Observable = routeGuard.canActivate(route, state); compositeCanActivateObservable = compositeCanActivateObservable.pipe( - tap(() => { - this.appLoadStateService.pushLoadingEvent(false); - }), - catchError(() => { - this.appLoadStateService.pushLoadingEvent(false); - return of(false); - }) + mergeMap(bool => { + if (!bool) { + return of(false); + } else { + return canActivateObservable; + } + }) ); - - return compositeCanActivateObservable; + } } + + compositeCanActivateObservable = compositeCanActivateObservable.pipe( + tap(() => { + this._appLoadStateService.pushLoadingEvent(false); + }), + catchError(() => { + this._appLoadStateService.pushLoadingEvent(false); + return of(false); + }) + ); + + return compositeCanActivateObservable; + } } diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index bc41964a8..7e0d34b4c 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -1,4 +1,12 @@ { + "auth-error": { + "heading": { + "label": "Your user doesn't have the required RED-* roles to access this application" + }, + "logout": { + "label": "Logout" + } + }, "manual-redaction": { "remove-annotation": { "success": {