Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/230 x role restriction #278

Merged
merged 9 commits into from
Apr 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20,493 changes: 20,483 additions & 10 deletions package-lock.json

Large diffs are not rendered by default.

137 changes: 137 additions & 0 deletions src/app/rest-role-interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { TestBed } from '@angular/core/testing';
import { HttpClient } from '@angular/common/http';
import { HttpTestingController } from '@angular/common/http/testing';
import { Router } from '@angular/router';

import { buildTestModuleMetadata } from 'src/spec-helpers';

describe('RestRoleInterceptor', () => {
let http: HttpClient;
let httpTestingController: HttpTestingController;
let successCallback: jasmine.Spy;
let errorCallback: jasmine.Spy;

const mockRouter = { url: '' };

beforeEach(() => {
TestBed.configureTestingModule(
buildTestModuleMetadata({
providers: [{ provide: Router, useValue: mockRouter }],
})
);

http = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);

successCallback = jasmine.createSpy('success');
errorCallback = jasmine.createSpy('error');
});

describe('.intercept', () => {
it('should not add header on root module', () => {
mockRouter.url = '/';
http.get('/').subscribe(successCallback, errorCallback);
httpTestingController
.expectOne(
(req) =>
req.url === '/' && req.headers.get('X-Role-Restriction') === null
)
.flush('hello', { status: 200, statusText: 'Success' });

expect(successCallback).toHaveBeenCalledWith('hello');
expect(errorCallback).not.toHaveBeenCalled();
});

it('should add header on presence control module', () => {
mockRouter.url = '/presence-control';
http.get('/presence-control').subscribe(successCallback, errorCallback);
httpTestingController
.expectOne(
(req) =>
req.url === '/presence-control' &&
req.headers.get('X-Role-Restriction') === 'LessonTeacherRole'
)
.flush('hello', { status: 200, statusText: 'Success' });

expect(successCallback).toHaveBeenCalledWith('hello');
expect(errorCallback).not.toHaveBeenCalled();
});

it('should add header on my absences module', () => {
mockRouter.url = '/my-absences';
http.get('/my-absences').subscribe(successCallback, errorCallback);
httpTestingController
.expectOne(
(req) =>
req.url === '/my-absences' &&
req.headers.get('X-Role-Restriction') === 'StudentRole'
)
.flush('hello', { status: 200, statusText: 'Success' });

expect(successCallback).toHaveBeenCalledWith('hello');
expect(errorCallback).not.toHaveBeenCalled();
});

it('should add header on open absences module', () => {
mockRouter.url = '/open-absences';
http.get('/open-absences').subscribe(successCallback, errorCallback);
httpTestingController
.expectOne(
(req) =>
req.url === '/open-absences' &&
req.headers.get('X-Role-Restriction') ===
'LessonTeacherRole;ClassTeacherRole'
)
.flush('hello', { status: 200, statusText: 'Success' });

expect(successCallback).toHaveBeenCalledWith('hello');
expect(errorCallback).not.toHaveBeenCalled();
});

it('should add header on edit absences module', () => {
mockRouter.url = '/edit-absences';
http.get('/edit-absences').subscribe(successCallback, errorCallback);
httpTestingController
.expectOne(
(req) =>
req.url === '/edit-absences' &&
req.headers.get('X-Role-Restriction') ===
'LessonTeacherRole;ClassTeacherRole'
)
.flush('hello', { status: 200, statusText: 'Success' });

expect(successCallback).toHaveBeenCalledWith('hello');
expect(errorCallback).not.toHaveBeenCalled();
});

it('should not add header on evaluate absences module', () => {
mockRouter.url = '/evaluate-absences';
http.get('/evaluate-absences').subscribe(successCallback, errorCallback);
httpTestingController
.expectOne(
(req) =>
req.url === '/evaluate-absences' &&
req.headers.get('X-Role-Restriction') === null
)
.flush('hello', { status: 200, statusText: 'Success' });

expect(successCallback).toHaveBeenCalledWith('hello');
expect(errorCallback).not.toHaveBeenCalled();
});

it('should not add header on my profile module', () => {
mockRouter.url = '/my-profile';
http.get('/my-profile').subscribe(successCallback, errorCallback);
httpTestingController
.expectOne(
(req) =>
req.url === '/my-profile' &&
req.headers.get('X-Role-Restriction') === null
)
.flush('hello', { status: 200, statusText: 'Success' });

expect(successCallback).toHaveBeenCalledWith('hello');
expect(errorCallback).not.toHaveBeenCalled();
});
});
});
51 changes: 51 additions & 0 deletions src/app/rest-role-interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Inject, Injectable } from '@angular/core';
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { SETTINGS, Settings } from './settings';
import { Router } from '@angular/router';
import { kebabToCamelCase } from 'codelyzer/util/utils';
import { getFirstSegment } from './shared/utils/url';

@Injectable()
export class RestRoleInterceptor implements HttpInterceptor {
constructor(
private router: Router,
@Inject(SETTINGS) private settings: Settings
) {}

/**
* Adds the X-Role-Restriction custom HTTP header for the given module to API requests.
*/
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
if (
!req.headers.has('X-Role-Restriction') &&
this.settings.headerRoleRestriction
) {
const module = this.getCurrentModuleName();
if (module && this.settings.headerRoleRestriction[module]) {
const headers = req.headers.set(
'X-Role-Restriction',
this.settings.headerRoleRestriction[module]
);
return next.handle(req.clone({ headers }));
}
}

return next.handle(req);
}

private getCurrentModuleName(): string | null {
const urlSegment = this.router.url
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das ist nur der Angular Teil oder? Dann finde ich dies eine gute Variante 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ja, genau. Alles nach dem #
Die ActivatedRoute enhält scheinbar nur in Komponenten die Infos zur Route.

? getFirstSegment(this.router.url)
: null;
return urlSegment ? kebabToCamelCase(urlSegment) : null;
}
}
1 change: 1 addition & 0 deletions src/app/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const Settings = t.type({
unconfirmedAbsencesRefreshTime: Option(t.number),
personMasterDataReportId: t.number,
studentConfirmationReportId: t.number,
headerRoleRestriction: t.record(t.string, t.string),
});

type Settings = t.TypeOf<typeof Settings>;
Expand Down
2 changes: 2 additions & 0 deletions src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { LetDirective } from './directives/let.directive';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { RestAuthInterceptor } from '../rest-auth-interceptor';
import { RestErrorInterceptor } from '../rest-error-interceptor';
import { RestRoleInterceptor } from '../rest-role-interceptor';
import { TypeaheadComponent } from './components/typeahead/typeahead.component';
import { DateSelectComponent } from './components/date-select/date-select.component';
import { SelectComponent } from './components/select/select.component';
Expand Down Expand Up @@ -50,6 +51,7 @@ const components = [
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: RestErrorInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: RestAuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: RestRoleInterceptor, multi: true },
],
imports: [
CommonModule,
Expand Down
21 changes: 20 additions & 1 deletion src/app/shared/utils/url.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseQueryString, serializeParams } from './url';
import { getFirstSegment, parseQueryString, serializeParams } from './url';

describe('url utilities', () => {
describe('parseQueryString', () => {
Expand Down Expand Up @@ -26,4 +26,23 @@ describe('url utilities', () => {
).toBe('foo=bar&baz=123&empty_value=&no_value');
});
});

describe('getFirstSegment', () => {
it('should get the first element of url with one segment', () => {
expect(getFirstSegment('/presence-control')).toBe('presence-control');
});
it('should get the first element of url with multiple segments', () => {
expect(getFirstSegment('/presence-control/student/3')).toBe(
'presence-control'
);
});
it('should get the first element of url with params', () => {
expect(getFirstSegment('/presence-control?date=2020-02-20')).toBe(
'presence-control'
);
});
it('should get the first element of root url', () => {
expect(getFirstSegment('/')).toBe(null);
});
});
});
19 changes: 18 additions & 1 deletion src/app/shared/utils/url.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Params } from '@angular/router';
import {
DefaultUrlSerializer,
Params,
PRIMARY_OUTLET,
UrlSegment,
UrlSegmentGroup,
} from '@angular/router';

/**
* Parses given query string to params object
Expand All @@ -23,3 +29,14 @@ export function serializeParams(params: Params): string {
}, [] as ReadonlyArray<string>)
.join('&');
}

/**
* Returns the first segment of the given url
*/
export function getFirstSegment(url: string): string | null {
const serializer = new DefaultUrlSerializer();
const tree = serializer.parse(url);
const g: UrlSegmentGroup = tree?.root.children[PRIMARY_OUTLET];
const s: UrlSegment[] = g?.segments;
return s ? s[0].path : null;
}
8 changes: 8 additions & 0 deletions src/settings.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,12 @@ window.absenzenmanagement.settings = {
// Id of the report that contains the open absences with
// confirmation values to sign (used in my absences)
studentConfirmationReportId: 30,

// X-Role-Restriction custom HTTP header values by module
headerRoleRestriction: {
myAbsences: 'StudentRole',
presenceControl: 'LessonTeacherRole',
openAbsences: 'LessonTeacherRole;ClassTeacherRole',
editAbsences: 'LessonTeacherRole;ClassTeacherRole',
},
};
6 changes: 6 additions & 0 deletions src/spec-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export const settings: Settings = {
unconfirmedAbsencesRefreshTime: null,
personMasterDataReportId: 290026,
studentConfirmationReportId: 30,
headerRoleRestriction: {
myAbsences: 'StudentRole',
presenceControl: 'LessonTeacherRole',
openAbsences: 'LessonTeacherRole;ClassTeacherRole',
editAbsences: 'LessonTeacherRole;ClassTeacherRole',
},
};

const baseTestModuleMetadata: TestModuleMetadata = {
Expand Down