Commit da7876b6 authored by François Agneray's avatar François Agneray
Browse files

Keycloak integration => ok

parent 1e9158e5
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './core/containers/login.component';
import { NotFoundPageComponent } from './core/containers/not-found-page.component';
import { UnauthorizedComponent } from './core/containers/unauthorized.component';
const routes: Routes = [
{ path: '', redirectTo: 'instance-list', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{ path: 'unauthorized', component: UnauthorizedComponent },
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: '**', component: NotFoundPageComponent }
];
......
......@@ -23,7 +23,9 @@ function initializeKeycloak(keycloak: KeycloakService, store: Store<{ keycloak:
clientId: environment.ssoClientId,
},
initOptions: {
onLoad: 'login-required'
onLoad: 'check-sso',
silentCheckSsoRedirectUri:
window.location.origin + '/assets/silent-check-sso.html'
},
loadUserProfileAtStartUp: true
});
......
......@@ -6,6 +6,7 @@ export const LOGIN = '[Auth] Login';
export const LOGOUT = '[Auth] Logout';
export const AUTH_SUCCESS = '[Auth] Auth Success';
export const LOAD_USER_PROFILE_SUCCESS = '[Auth] Load User Profile Success';
export const LOAD_USER_ROLES_SUCCESS = '[Auth] Load User Roles Success';
export const OPEN_EDIT_PROFILE = '[Auth] Edit Profile';
export class LoginAction implements Action {
......@@ -32,6 +33,12 @@ export class LoadUserProfileSuccessAction implements Action {
constructor(public payload: UserProfile) { }
}
export class LoadUserRolesSuccessAction implements Action {
readonly type = LOAD_USER_ROLES_SUCCESS;
constructor(public payload: string[]) { }
}
export class OpenEditProfileAction implements Action {
readonly type = OPEN_EDIT_PROFILE;
......@@ -43,4 +50,5 @@ export type Actions
| LogoutAction
| AuthSuccessAction
| LoadUserProfileSuccessAction
| LoadUserRolesSuccessAction
| OpenEditProfileAction;
......@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { from } from 'rxjs';
import { switchMap, map, tap } from 'rxjs/operators';
import { Action } from '@ngrx/store';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { KeycloakService } from 'keycloak-angular';
......@@ -24,7 +25,7 @@ export class AuthEffects {
@Effect({ dispatch: false })
logoutAction$ = this.actions$.pipe(
ofType(authActions.LOGOUT),
tap(_ => this.keycloak.logout())
tap(_ => this.keycloak.logout(window.location.origin + '/login'))
);
@Effect()
......@@ -32,7 +33,10 @@ export class AuthEffects {
ofType(authActions.AUTH_SUCCESS),
switchMap(_ =>
from(this.keycloak.loadUserProfile()).pipe(
map(userProfile => new authActions.LoadUserProfileSuccessAction(userProfile))
switchMap(userProfile => [
new authActions.LoadUserProfileSuccessAction(userProfile),
new authActions.LoadUserRolesSuccessAction(this.keycloak.getUserRoles())
])
)
)
);
......
import * as actions from './auth.action';
import { UserProfile } from './user-profile.model';
export interface State {
isAuthenticated: boolean;
userProfile: any;
userProfile: UserProfile;
userRoles: string[];
}
export const initialState: State = {
isAuthenticated: false,
userProfile: null
userProfile: null,
userRoles: []
};
export function reducer(state: State = initialState, action: actions.Actions): State {
......@@ -24,7 +28,15 @@ export function reducer(state: State = initialState, action: actions.Actions): S
return {
...state,
userProfile
}
};
case actions.LOAD_USER_ROLES_SUCCESS:
const userRoles = action.payload;
return {
...state,
userRoles
};
default:
return state;
......@@ -33,3 +45,4 @@ export function reducer(state: State = initialState, action: actions.Actions): S
export const isAuthenticated = (state: State) => state.isAuthenticated;
export const getUserProfile = (state: State) => state.userProfile;
export const getUserRoles = (state: State) => state.userRoles;
......@@ -2,14 +2,19 @@ import { createSelector, createFeatureSelector } from '@ngrx/store';
import * as auth from './auth.reducer';
export const getAuthtate = createFeatureSelector<auth.State>('auth');
export const getAuthState = createFeatureSelector<auth.State>('auth');
export const isAuthenticated = createSelector(
getAuthtate,
getAuthState,
auth.isAuthenticated
);
export const getUserProfile = createSelector(
getAuthtate,
getAuthState,
auth.getUserProfile
);
export const getUserRoles = createSelector(
getAuthState,
auth.getUserRoles
);
......@@ -23,20 +23,26 @@ export class AuthGuard extends KeycloakAuthGuard {
) {
// Force the user to log in if currently unauthenticated.
if (!this.authenticated) {
await this.keycloak.login({
redirectUri: window.location.origin + state.url,
});
this.router.navigateByUrl('/login');
}
// If authenticated but not admin go to unauthorized page.
if(!this.roles.includes('anis_admin')) {
this.router.navigateByUrl('/unauthorized');
}
// Else return true;
return true;
// Get the roles required from the route.
const requiredRoles = route.data.roles;
//const requiredRoles = route.data.roles;
// Allow the user to to proceed if no additional roles are required to access the route.
if (!(requiredRoles instanceof Array) || requiredRoles.length === 0) {
/* if (!(requiredRoles instanceof Array) || requiredRoles.length === 0) {
return true;
}
} */
// Allow the user to proceed if all the required roles are present.
return requiredRoles.every((role) => this.roles.includes(role));
//return requiredRoles.every((role) => this.roles.includes(role));
}
}
\ No newline at end of file
<header>
<app-nav [userProfile]="userProfile | async" (logout)="logout()" (openEditProfile)="openEditProfile()">
<app-nav *ngIf="(isAuthenticated | async) && (isAnisAdmin() | async)" [userProfile]="userProfile | async" (logout)="logout()" (openEditProfile)="openEditProfile()">
</app-nav>
</header>
<main role="main" class="pb-4">
<router-outlet></router-outlet>
</main>
<footer class="footer mt-auto bg-light">
<footer *ngIf="(isAuthenticated | async) && (isAnisAdmin() | async)" class="footer mt-auto bg-light">
<div class="container my-3">
<div class="row justify-content-center mb-3">
<small>&copy; ANIS 2014 - {{ year }}</small>
......
......@@ -2,6 +2,7 @@ import { Component, ViewEncapsulation } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as fromAuth from '../../auth/store/auth.reducer';
import * as authActions from '../../auth/store/auth.action';
......@@ -21,11 +22,13 @@ export class AppComponent {
anisClientVersion: string = VERSIONS.anisClient;
year = (new Date()).getFullYear();
isAuthenticated: Observable<boolean>;
userProfile: Observable<UserProfile>
userProfile: Observable<UserProfile>;
userRoles: Observable<string[]>;
constructor(private store: Store<{ auth: fromAuth.State }>) {
this.isAuthenticated = store.select(authSelector.isAuthenticated);
this.userProfile = store.select(authSelector.getUserProfile);
this.userRoles = store.select(authSelector.getUserRoles);
}
logout(): void {
......@@ -35,4 +38,10 @@ export class AppComponent {
openEditProfile(): void {
this.store.dispatch(new authActions.OpenEditProfileAction());
}
isAnisAdmin() {
return this.userRoles.pipe(
map(roles => roles.includes('anis_admin'))
);
}
}
<div class="text-center">
<img class="mb-4" src="assets/anis_adminsi.png" alt="">
<p class="text-center">
You must be logged in and have the right privileges to access the administration interface.
</p>
<p class="text-center"><button type="button" class="btn btn-outline-primary" (click)="login()">Sign in</button></p>
<p class="mt-5 mb-3 text-muted"><small>&copy; ANIS ADMIN 2014 - {{ year }}</small></p>
</div>
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import * as fromAuth from '../../auth/store/auth.reducer';
import * as authActions from '../../auth/store/auth.action';
import * as authSelector from '../../auth/store/auth.selector';
@Component({
selector: 'app-login',
templateUrl: 'login.component.html'
})
export class LoginComponent implements OnInit {
year = (new Date()).getFullYear();
public isAuthenticated: Observable<boolean>;
constructor(private store: Store<{ auth: fromAuth.State }>, private router: Router) {
this.isAuthenticated = store.select(authSelector.isAuthenticated);
}
ngOnInit() {
this.isAuthenticated.subscribe(isAuthenticated => (isAuthenticated) ? this.router.navigateByUrl('/instance-list') : null );
}
login(): void {
this.store.dispatch(new authActions.LoginAction());
}
}
<div class="text-center">
<img class="mb-4" src="assets/anis_adminsi.png" alt="">
<p>
You are not authorized to navigate to this interface (403).<br />
Please contact the administrator to increase your access rights.
</p>
<p class="mt-5 mb-3 text-muted"><small>&copy; ANIS ADMIN 2014 - {{ year }}</small></p>
</div>
\ No newline at end of file
import { Component } from '@angular/core';
@Component({
selector: 'app-unauthorized',
templateUrl: 'unauthorized.component.html'
})
export class UnauthorizedComponent {
year = (new Date()).getFullYear();
}
......@@ -6,12 +6,14 @@ import { CollapseModule } from 'ngx-bootstrap/collapse';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { AppComponent } from './containers/app.component';
import { LoginComponent } from './containers/login.component';
import { NotFoundPageComponent } from './containers/not-found-page.component';
import { NavComponent } from './components/nav.component';
import { throwIfAlreadyLoaded } from './module-import-guard';
export const COMPONENTS = [
AppComponent,
LoginComponent,
NotFoundPageComponent,
NavComponent
];
......
......@@ -18,28 +18,24 @@ import { AttributeComponent } from './containers/attribute/attribute.component';
import { AuthGuard } from '../core/auth.guard';
const routes: Routes = [
{ path: 'instance-list', canActivate: [AuthGuard], component: InstanceComponent },
{ path: 'new-instance', canActivate: [AuthGuard], component: NewInstanceComponent },
{ path: 'edit-instance/:iname', canActivate: [AuthGuard], component: EditInstanceComponent },
{ path: 'configure-instance/:iname', canActivate: [AuthGuard], component: ConfigureInstanceComponent },
{ path: 'configure-instance/:iname/new-dataset', canActivate: [AuthGuard], component: NewDatasetComponent },
{ path: 'configure-instance/:iname/edit-dataset/:dname', canActivate: [AuthGuard], component: EditDatasetComponent },
{ path: 'configure-instance/:iname/configure-dataset/:dname', canActivate: [AuthGuard], component: AttributeComponent },
{
path: '', canActivate: [AuthGuard], data: { roles: ['anis_admin']}, children: [
{ path: 'instance-list', component: InstanceComponent },
{ path: 'new-instance', component: NewInstanceComponent },
{ path: 'edit-instance/:iname', component: EditInstanceComponent },
{ path: 'configure-instance/:iname', component: ConfigureInstanceComponent },
{ path: 'configure-instance/:iname/new-dataset', component: NewDatasetComponent },
{ path: 'configure-instance/:iname/edit-dataset/:dname', component: EditDatasetComponent },
{ path: 'configure-instance/:iname/configure-dataset/:dname', component: AttributeComponent },
{
path: 'project', component: ProjectPageComponent, children: [
{ path: '', redirectTo: 'project-list', pathMatch: 'full' },
{ path: 'project-list', component: ProjectComponent },
{ path: 'database-list', component: DatabaseComponent }
]
},
{ path: 'new-project', component: NewProjectComponent },
{ path: 'edit-project/:name', component: EditProjectComponent },
{ path: 'new-database', component: NewDatabaseComponent },
{ path: 'edit-database/:id', component: EditDatabaseComponent }
path: 'project', component: ProjectPageComponent, canActivate: [AuthGuard], children: [
{ path: '', redirectTo: 'project-list', pathMatch: 'full' },
{ path: 'project-list', component: ProjectComponent },
{ path: 'database-list', component: DatabaseComponent }
]
}
},
{ path: 'new-project', canActivate: [AuthGuard], component: NewProjectComponent },
{ path: 'edit-project/:name', canActivate: [AuthGuard], component: EditProjectComponent },
{ path: 'new-database', canActivate: [AuthGuard], component: NewDatabaseComponent },
{ path: 'edit-database/:id', canActivate: [AuthGuard], component: EditDatabaseComponent }
];
@NgModule({
......
......@@ -2,10 +2,11 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SettingsComponent } from './containers/settings.component';
import { AuthGuard } from '../core/auth.guard';
const routes: Routes = [
{ path: 'settings', component: SettingsComponent },
{ path: 'settings/:select', component: SettingsComponent }
{ path: 'settings', canActivate: [AuthGuard], component: SettingsComponent },
{ path: 'settings/:select', canActivate: [AuthGuard], component: SettingsComponent }
];
@NgModule({
......
<html>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment