Skip to content

Commit 4d6de8f

Browse files
committed
feat(admin): Add activate guard
* created routing guard * added guard for organizations section for some common scenarios * rest off the section as well as pages need to be added
1 parent 02623f3 commit 4d6de8f

File tree

10 files changed

+230
-1
lines changed

10 files changed

+230
-1
lines changed

apps/admin-gui/src/app/app-routing.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { NotFoundPageComponent } from './shared/components/not-found-page/not-fo
66
import { RedirectPageComponent } from '@perun-web-apps/perun/components';
77
import { LoginScreenComponent } from '@perun-web-apps/perun/login';
88
import { LoginScreenServiceAccessComponent } from '@perun-web-apps/perun/login';
9+
import { NotAuthorizedPageComponent } from './shared/components/not-authorized-page/not-authorized-page.component';
910

1011
const routes: Routes = [
1112
{
@@ -49,6 +50,10 @@ const routes: Routes = [
4950
path: 'home',
5051
component: UserDashboardComponent,
5152
},
53+
{
54+
path: 'notAuthorized',
55+
component: NotAuthorizedPageComponent,
56+
},
5257
{ path: '**', component: NotFoundPageComponent },
5358
];
5459

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div class="flex-container pl-xl-5 pr-xl-5">
2+
<h1 class="page-title">{{'GENERAL.NOT_AUTHORIZED_PAGE.TITLE' | translate}}</h1>
3+
<span>{{'GENERAL.NOT_AUTHORIZED_PAGE.DESC' | translate}}</span>
4+
<button mat-stroked-button (click)="redirectToHome()">
5+
{{'GENERAL.NOT_AUTHORIZED_PAGE.REDIRECT' | translate}}
6+
</button>
7+
</div>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.flex-container {
2+
display: flex;
3+
flex-direction: column;
4+
align-items: flex-start;
5+
gap: 1em;
6+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Component } from '@angular/core';
2+
import { Router } from '@angular/router';
3+
4+
@Component({
5+
selector: 'app-not-authorized-page',
6+
templateUrl: './not-authorized-page.component.html',
7+
styleUrls: ['./not-authorized-page.component.scss'],
8+
})
9+
export class NotAuthorizedPageComponent {
10+
constructor(private router: Router) {}
11+
12+
redirectToHome(): void {
13+
void this.router.navigate(['/home'], { queryParamsHandling: 'merge' });
14+
}
15+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Injectable } from '@angular/core';
2+
import {
3+
ActivatedRouteSnapshot,
4+
CanActivateChild,
5+
Router,
6+
RouterStateSnapshot,
7+
UrlTree,
8+
} from '@angular/router';
9+
import { Observable } from 'rxjs';
10+
import { GuiAuthResolver, NotificatorService } from '@perun-web-apps/perun/services';
11+
import { PerunBean } from '@perun-web-apps/perun/openapi';
12+
import { RoutePolicyService } from '../../../../../libs/perun/services/src/lib/route-policy.service';
13+
14+
interface AuthPair {
15+
key: string;
16+
entity: PerunBean;
17+
}
18+
19+
@Injectable({
20+
providedIn: 'root',
21+
})
22+
export class RouteAuthGuardService implements CanActivateChild {
23+
constructor(
24+
private authResolver: GuiAuthResolver,
25+
private routePolicyService: RoutePolicyService,
26+
private router: Router,
27+
private notificator: NotificatorService
28+
) {}
29+
30+
private static getBeanName(key: string): string {
31+
switch (key) {
32+
case 'o':
33+
return 'Vo';
34+
case 'g':
35+
return 'Group';
36+
case 'f':
37+
return 'Facility';
38+
case 'r':
39+
return 'Resource';
40+
default:
41+
return '';
42+
}
43+
}
44+
45+
private static parseUrl(url: string): AuthPair {
46+
const segments: string[] = url.slice(1).split('/').reverse();
47+
const authPair: AuthPair = { key: '', entity: { id: -1, beanName: '' } };
48+
49+
for (const segment of segments) {
50+
if (Number(segment)) {
51+
if (authPair.entity.id === -1) {
52+
authPair.entity.id = Number(segment);
53+
continue;
54+
}
55+
break;
56+
}
57+
58+
authPair.key = segment.concat('-', authPair.key);
59+
}
60+
61+
authPair.key = authPair.key.slice(0, authPair.key.length - 1);
62+
authPair.entity.beanName = RouteAuthGuardService.getBeanName(authPair.key[0]);
63+
return authPair;
64+
}
65+
66+
canActivateChild(
67+
_childRoute: ActivatedRouteSnapshot,
68+
state: RouterStateSnapshot
69+
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
70+
if (this.authResolver.isPerunAdminOrObserver()) {
71+
return true;
72+
}
73+
74+
const authPair: AuthPair = RouteAuthGuardService.parseUrl(state.url);
75+
const isAuthorized: boolean = this.routePolicyService.canNavigate(
76+
authPair.key,
77+
authPair.entity
78+
);
79+
80+
if (isAuthorized) {
81+
return true;
82+
}
83+
84+
this.notificator.showRouteError();
85+
return this.router.parseUrl('/notAuthorized');
86+
}
87+
}

apps/admin-gui/src/app/shared/shared.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ import { PerunNamespacePasswordFormModule } from '@perun-web-apps/perun/namespac
169169
import { AuditMessagesListComponent } from './components/audit-messages-list/audit-messages-list.component';
170170
import { AuditMessageDetailDialogComponent } from './components/dialogs/audit-message-detail-dialog/audit-message-detail-dialog.component';
171171
import { ParseEventNamePipe } from './pipes/parse-event-name.pipe';
172+
import { NotAuthorizedPageComponent } from './components/not-authorized-page/not-authorized-page.component';
172173

173174
@NgModule({
174175
imports: [
@@ -430,6 +431,7 @@ import { ParseEventNamePipe } from './pipes/parse-event-name.pipe';
430431
AuditMessagesListComponent,
431432
AuditMessageDetailDialogComponent,
432433
ParseEventNamePipe,
434+
NotAuthorizedPageComponent,
433435
],
434436
providers: [AnyToStringPipe, ExtSourceTypePipe],
435437
})

apps/admin-gui/src/app/vos/vos-routing.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { GroupStatisticsComponent } from './pages/group-detail-page/group-statis
5959
import { ApplicationFormManageGroupsComponent } from './components/application-form-manage-groups/application-form-manage-groups.component';
6060
import { ResourceTagsComponent } from '../facilities/pages/resource-detail-page/resource-tags/resource-tags.component';
6161
import { VoSettingsServiceMembersComponent } from './pages/vo-detail-page/vo-settings/vo-settings-service-members/vo-settings-service-members.component';
62+
import { RouteAuthGuardService } from '../shared/route-auth-guard.service';
6263

6364
const routes: Routes = [
6465
{
@@ -68,6 +69,7 @@ const routes: Routes = [
6869
{
6970
path: ':voId',
7071
component: VoDetailPageComponent,
72+
canActivateChild: [RouteAuthGuardService],
7173
children: [
7274
{
7375
path: '',

apps/admin-gui/src/assets/i18n/en.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
"TITLE": "Page not found",
1313
"MESSAGE": "The page you are trying to access is missing. If you wish to continue please click the button below that will redirect you to our home page.",
1414
"ACTION": "Redirect"
15+
},
16+
"NOT_AUTHORIZED_PAGE": {
17+
"TITLE": "Not authorized",
18+
"DESC": "You are not authorized to access the page you were trying to reach.",
19+
"REDIRECT": "Redirect Home"
1520
}
1621
},
1722
"NAV": {
@@ -2604,7 +2609,9 @@
26042609
"DIALOG_CLOSE": "Close",
26052610
"DIALOG_BUG_REPORT": "Report a bug",
26062611
"DEFAULT_RPC_ERROR_MESSAGE": "An operation failed.",
2607-
"PRIVILEGE_EXCEPTION": "You are not authorized to perform this action"
2612+
"PRIVILEGE_EXCEPTION": "You are not authorized to perform this action",
2613+
"ROUTE_DENIED_ERROR": "Access denied",
2614+
"ROUTE_DENIED_DESC": "You are not authorized to access this page"
26082615
}
26092616
},
26102617
"USER_EXT_SOURCES_LIST": {

libs/perun/services/src/lib/notificator.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ export class NotificatorService {
6060
}
6161
}
6262

63+
showRouteError(): void {
64+
const title: string = this.translate.instant(
65+
'SHARED_LIB.PERUN.COMPONENTS.NOTIFICATOR.NOTIFICATION.ROUTE_DENIED_ERROR'
66+
);
67+
const desc: string = this.translate.instant(
68+
'SHARED_LIB.PERUN.COMPONENTS.NOTIFICATOR.NOTIFICATION.ROUTE_DENIED_DESC'
69+
);
70+
this.showError(title, null, desc);
71+
}
72+
6373
/**
6474
* Shows error notification
6575
*
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Injectable } from '@angular/core';
2+
import { Facility, Group, Member, Resource, User, Vo } from '@perun-web-apps/perun/openapi';
3+
import { GuiAuthResolver } from './gui-auth-resolver.service';
4+
5+
type Entity = Vo | Group | Resource | Facility | Member | User;
6+
7+
@Injectable({
8+
providedIn: 'root',
9+
})
10+
export class RoutePolicyService {
11+
constructor(private authResolver: GuiAuthResolver) {}
12+
13+
// Map of page key and function determining if user is authorized to access given page
14+
private routePolicies: Map<string, (entity: Entity) => boolean> = new Map<
15+
string,
16+
(entity: Entity) => boolean
17+
>([
18+
[
19+
'organizations-members',
20+
(vo) => this.authResolver.isAuthorized('getCompleteRichMembers_Vo_List<String>_policy', [vo]),
21+
],
22+
[
23+
'organizations-groups',
24+
(vo) =>
25+
this.authResolver.isAuthorized(
26+
'getAllRichGroupsWithAttributesByNames_Vo_List<String>_policy',
27+
[vo]
28+
),
29+
],
30+
[
31+
'organization-resources',
32+
(vo) => this.authResolver.isAuthorized('getRichResources_Vo_policy', [vo]),
33+
],
34+
[
35+
'organizations-applications',
36+
(vo) =>
37+
this.authResolver.isAuthorized('getApplicationsForVo_Vo_List<String>_Boolean_policy', [vo]),
38+
],
39+
[
40+
'organizations-sponsoredMembers',
41+
(vo) => this.authResolver.isAuthorized('getSponsoredMembersAndTheirSponsors_Vo_policy', [vo]),
42+
],
43+
[
44+
'organizations-serviceAccounts',
45+
(vo) =>
46+
this.authResolver.isAuthorized(
47+
`createSpecificMember_Vo_Candidate_List<User>_SpecificUserType_List<Group>_policy`,
48+
[vo]
49+
),
50+
],
51+
['organizations-attributes', () => true],
52+
[
53+
'organizations-settings',
54+
(vo) =>
55+
this.authResolver.isManagerPagePrivileged(vo) ||
56+
this.authResolver.isAuthorized('getVoExtSources_Vo_policy', [vo]) ||
57+
this.authResolver.isThisVoAdminOrObserver(vo.id),
58+
],
59+
['organizations-settings-expiration', (vo) => this.authResolver.isThisVoAdminOrObserver(vo.id)],
60+
['organizations-settings-managers', (vo) => this.authResolver.isManagerPagePrivileged(vo)],
61+
[
62+
'organizations-settings-applicationForm',
63+
(vo) => this.authResolver.isThisVoAdminOrObserver(vo.id),
64+
],
65+
[
66+
'organizations-settings-notifications',
67+
(vo) => this.authResolver.isThisVoAdminOrObserver(vo.id),
68+
],
69+
[
70+
'organizations-settings-extsources',
71+
(vo) => this.authResolver.isAuthorized('getVoExtSources_Vo_policy', [vo]),
72+
],
73+
]);
74+
75+
/**
76+
* Determines whether user can access given page or not,
77+
* default is true
78+
*
79+
* @param key: page key
80+
* @param entity: entity connected to given page
81+
*
82+
* @returns true if user can access page, false otherwise
83+
* */
84+
canNavigate(key: string, entity: Entity): boolean {
85+
const authorize: (e: Entity) => boolean = this.routePolicies.get(key);
86+
return authorize ? authorize(entity) : true;
87+
}
88+
}

0 commit comments

Comments
 (0)