fix(#338): Implement proper system admin role separate from workspace ownership
- Replace workspace ownership check with explicit SYSTEM_ADMIN_IDS env var - System admin access is now explicit and configurable via environment - Workspace owners no longer automatically get system admin privileges - Add 15 unit tests verifying security separation - Add SYSTEM_ADMIN_IDS documentation to .env.example Refs #338 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,14 @@
|
||||
* Admin Guard
|
||||
*
|
||||
* Restricts access to system-level admin operations.
|
||||
* Currently checks if user owns at least one workspace (indicating admin status).
|
||||
* Future: Replace with proper role-based access control (RBAC).
|
||||
* System administrators are configured via the SYSTEM_ADMIN_IDS environment variable.
|
||||
*
|
||||
* Configuration:
|
||||
* SYSTEM_ADMIN_IDS=uuid1,uuid2,uuid3 (comma-separated list of user IDs)
|
||||
*
|
||||
* Note: Workspace ownership does NOT grant system admin access. These are separate concepts:
|
||||
* - Workspace owner: Can manage their workspace and its members
|
||||
* - System admin: Can perform system-level operations across all workspaces
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -13,16 +19,42 @@ import {
|
||||
ForbiddenException,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import { PrismaService } from "../../prisma/prisma.service";
|
||||
import type { AuthenticatedRequest } from "../../common/types/user.types";
|
||||
|
||||
@Injectable()
|
||||
export class AdminGuard implements CanActivate {
|
||||
private readonly logger = new Logger(AdminGuard.name);
|
||||
private readonly systemAdminIds: Set<string>;
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor() {
|
||||
// Load system admin IDs from environment variable
|
||||
const adminIdsEnv = process.env.SYSTEM_ADMIN_IDS ?? "";
|
||||
this.systemAdminIds = new Set(
|
||||
adminIdsEnv
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id.length > 0)
|
||||
);
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
if (this.systemAdminIds.size === 0) {
|
||||
this.logger.warn(
|
||||
"No system administrators configured. Set SYSTEM_ADMIN_IDS environment variable."
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`System administrators configured: ${String(this.systemAdminIds.size)} user(s)`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user ID is a system administrator
|
||||
*/
|
||||
isSystemAdmin(userId: string): boolean {
|
||||
return this.systemAdminIds.has(userId);
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
|
||||
const user = request.user;
|
||||
|
||||
@@ -30,13 +62,7 @@ export class AdminGuard implements CanActivate {
|
||||
throw new ForbiddenException("User not authenticated");
|
||||
}
|
||||
|
||||
// Check if user owns any workspace (admin indicator)
|
||||
// TODO: Replace with proper RBAC system admin role check
|
||||
const ownedWorkspaces = await this.prisma.workspace.count({
|
||||
where: { ownerId: user.id },
|
||||
});
|
||||
|
||||
if (ownedWorkspaces === 0) {
|
||||
if (!this.isSystemAdmin(user.id)) {
|
||||
this.logger.warn(`Non-admin user ${user.id} attempted admin operation`);
|
||||
throw new ForbiddenException("This operation requires system administrator privileges");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user