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:
Jason Woltje
2026-02-05 16:44:50 -06:00
parent 32c81e96cf
commit 06de72a355
3 changed files with 214 additions and 12 deletions

View File

@@ -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");
}