Introduce HasPermission annotation (#2693)
* Introduce HasPermission annotation * Update changelog
This commit is contained in:
parent
1d796a9597
commit
813e73a0a3
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added a historical cash balances table to the account detail dialog
|
- Added a historical cash balances table to the account detail dialog
|
||||||
|
- Introduced a `HasPermission` annotation for endpoints
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
6
apps/api/src/decorators/has-permission.decorator.ts
Normal file
6
apps/api/src/decorators/has-permission.decorator.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
export const HAS_PERMISSION_KEY = 'has_permission';
|
||||||
|
|
||||||
|
export function HasPermission(permission: string) {
|
||||||
|
return SetMetadata(HAS_PERMISSION_KEY, permission);
|
||||||
|
}
|
55
apps/api/src/guards/has-permission.guard.spec.ts
Normal file
55
apps/api/src/guards/has-permission.guard.spec.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { HttpException } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { HasPermissionGuard } from './has-permission.guard';
|
||||||
|
|
||||||
|
describe('HasPermissionGuard', () => {
|
||||||
|
let guard: HasPermissionGuard;
|
||||||
|
let reflector: Reflector;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [HasPermissionGuard, Reflector]
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
guard = module.get<HasPermissionGuard>(HasPermissionGuard);
|
||||||
|
reflector = module.get<Reflector>(Reflector);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupReflectorSpy(returnValue: string) {
|
||||||
|
jest.spyOn(reflector, 'get').mockReturnValue(returnValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockExecutionContext(permissions: string[]) {
|
||||||
|
return new ExecutionContextHost([
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
permissions // Set user permissions based on the argument
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should deny access if the user does not have any permission', () => {
|
||||||
|
setupReflectorSpy('required-permission');
|
||||||
|
const noPermissions = createMockExecutionContext([]);
|
||||||
|
|
||||||
|
expect(() => guard.canActivate(noPermissions)).toThrow(HttpException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deny access if the user has the wrong permission', () => {
|
||||||
|
setupReflectorSpy('required-permission');
|
||||||
|
const wrongPermission = createMockExecutionContext(['wrong-permission']);
|
||||||
|
|
||||||
|
expect(() => guard.canActivate(wrongPermission)).toThrow(HttpException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow access if the user has the required permission', () => {
|
||||||
|
setupReflectorSpy('required-permission');
|
||||||
|
const rightPermission = createMockExecutionContext(['required-permission']);
|
||||||
|
|
||||||
|
expect(guard.canActivate(rightPermission)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
37
apps/api/src/guards/has-permission.guard.ts
Normal file
37
apps/api/src/guards/has-permission.guard.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { HAS_PERMISSION_KEY } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||||
|
import { hasPermission } from '@ghostfolio/common/permissions';
|
||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
HttpException,
|
||||||
|
Injectable
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HasPermissionGuard implements CanActivate {
|
||||||
|
public constructor(private reflector: Reflector) {}
|
||||||
|
|
||||||
|
public canActivate(context: ExecutionContext): boolean {
|
||||||
|
const requiredPermission = this.reflector.get<string>(
|
||||||
|
HAS_PERMISSION_KEY,
|
||||||
|
context.getHandler()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!requiredPermission) {
|
||||||
|
return true; // No specific permissions required
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
if (!user || !hasPermission(user.permissions, requiredPermission)) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user