authentication authorization
This commit is contained in:
91
apps/api/src/domain/policy/FeatureAvailabilityGuard.ts
Normal file
91
apps/api/src/domain/policy/FeatureAvailabilityGuard.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ServiceUnavailableException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PolicyService, ActionType, FeatureState } from './PolicyService';
|
||||
import { FEATURE_AVAILABILITY_METADATA_KEY, FeatureAvailabilityMetadata } from './RequireCapability';
|
||||
|
||||
type Evaluation = { allow: true } | { allow: false; reason: 'maintenance' | FeatureState | 'not_configured' };
|
||||
|
||||
@Injectable()
|
||||
export class FeatureAvailabilityGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly policyService: PolicyService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const handler = context.getHandler();
|
||||
const controllerClass = context.getClass();
|
||||
|
||||
const metadata =
|
||||
this.reflector.getAllAndOverride<FeatureAvailabilityMetadata | undefined>(
|
||||
FEATURE_AVAILABILITY_METADATA_KEY,
|
||||
[handler, controllerClass],
|
||||
) ?? null;
|
||||
|
||||
if (!metadata) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const snapshot = await this.policyService.getSnapshot();
|
||||
const decision = evaluate(snapshot, metadata);
|
||||
|
||||
if (decision.allow) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (decision.reason === 'maintenance') {
|
||||
throw new ServiceUnavailableException('Service temporarily unavailable');
|
||||
}
|
||||
|
||||
throw new NotFoundException('Not Found');
|
||||
}
|
||||
}
|
||||
|
||||
function evaluate(
|
||||
snapshot: Awaited<ReturnType<PolicyService['getSnapshot']>>,
|
||||
metadata: FeatureAvailabilityMetadata,
|
||||
): Evaluation {
|
||||
if (snapshot.operationalMode === 'maintenance') {
|
||||
const allowlist = metadata.actionType === 'mutate'
|
||||
? snapshot.maintenanceAllowlist.mutate
|
||||
: snapshot.maintenanceAllowlist.view;
|
||||
|
||||
if (!allowlist.includes(metadata.capabilityKey)) {
|
||||
return { allow: false, reason: 'maintenance' };
|
||||
}
|
||||
}
|
||||
|
||||
const state = snapshot.capabilities[metadata.capabilityKey] ?? 'hidden';
|
||||
|
||||
if (state === 'enabled') {
|
||||
return { allow: true };
|
||||
}
|
||||
|
||||
// Coming soon is treated as "not found" on the public API for now (no disclosure).
|
||||
if (state === 'coming_soon') {
|
||||
return { allow: false, reason: 'coming_soon' };
|
||||
}
|
||||
|
||||
if (state === 'disabled' || state === 'hidden') {
|
||||
return { allow: false, reason: state };
|
||||
}
|
||||
|
||||
return { allow: false, reason: 'not_configured' };
|
||||
}
|
||||
|
||||
export function inferActionTypeFromHttpMethod(method: string): ActionType {
|
||||
switch (method.toUpperCase()) {
|
||||
case 'GET':
|
||||
case 'HEAD':
|
||||
case 'OPTIONS':
|
||||
return 'view';
|
||||
default:
|
||||
return 'mutate';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user