0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added auth/permissions nest concepts

This commit is contained in:
Fabien "egg" O'Carroll 2023-12-12 16:44:24 +07:00
parent f8555100bc
commit 61a33fdff7
3 changed files with 134 additions and 0 deletions

View file

@ -0,0 +1,8 @@
import {Reflector} from '@nestjs/core';
type UserRole = 'Contributor' | 'Author' | 'Editor' | 'Admin' | 'Owner';
type APIKeyRole = 'Admin Integration' | 'Ghost Explore Integration' | 'Self-Serve Migration Integration' | 'DB Backup Integration' | 'Scheduler Integration';
export type Role = UserRole | APIKeyRole;
export const Roles = Reflector.createDecorator<Role[]>();

View file

@ -0,0 +1,100 @@
import {
Injectable,
CanActivate,
ExecutionContext,
Inject
} from '@nestjs/common';
import {Request, Response} from 'express';
import {Actor} from '../../common/types/actor.type';
import ObjectID from 'bson-objectid';
// Here we extend the express Request interface with our new type
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
// eslint-disable-next-line no-shadow
export interface Request {
actor?: Actor
}
}
}
interface SessionService {
// We use any because we've not got types for bookshelf models
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getUserForSession(req: Request, res: Response): Promise<any>
}
interface AuthenticationService {
// We use any because we've not got types for bookshelf models
// eslint-disable-next-line @typescript-eslint/no-explicit-any
authenticateWithToken(url: string, token: string, ignoreMaxAge: boolean): Promise<any>
}
@Injectable()
export class AdminAPIAuthentication implements CanActivate {
constructor(
@Inject('SessionService') private sessionService: SessionService,
@Inject('AdminAuthenticationService') private authService: AuthenticationService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();
const user = await this.sessionService.getUserForSession(request, response);
if (user) {
await this.setUserActor(user, request);
return true;
}
if (!request.headers || !request.headers.authorization) {
return false;
}
const [scheme, token] = request.headers.authorization.split(' ');
if (!/^Ghost$/i.test(scheme)) {
return false;
}
const {apiKey, user: apiUser} = await this.authService.authenticateWithToken(
request.originalUrl,
token,
false
);
if (user) {
await this.setUserActor(apiUser, request);
return true;
}
if (apiKey) {
await apiKey.related('role').fetch();
const json = apiKey.toJSON();
request.actor = {
id: ObjectID.createFromHexString(json.integration.id),
role: json.role.name,
type: 'api_key'
};
return true;
}
return false;
}
// This is `any` because again it represents a bookshelf model
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private async setUserActor(user: any, request: Request) {
await user.related('roles').fetch();
const json = user.toJSON();
request.actor = {
// BS To work around Owner id === 1
id: ObjectID.createFromHexString(json.id === '1' ? 'abcd12345678901234567890' : json.id),
role: json.roles[0].name,
type: 'user'
};
}
}

View file

@ -0,0 +1,26 @@
import {
Injectable,
CanActivate,
ExecutionContext,
Inject
} from '@nestjs/common';
import {Reflector} from '@nestjs/core';
import {Roles} from '../../common/decorators/permissions.decorator';
import {Request} from 'express';
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(@Inject(Reflector) private reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const roles = this.reflector.get(Roles, context.getHandler());
const request = context.switchToHttp().getRequest<Request>();
const role = request.actor?.role;
if (role && roles.includes(role)) {
return true;
}
return false;
}
}