Files
mcpctl/src/mcpd/src/services/auth.service.ts
Michal dcda93d179
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / typecheck (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / package (pull_request) Has been cancelled
feat: granular RBAC with resource/operation bindings, users, groups
- Replace admin role with granular roles: view, create, delete, edit, run
- Two binding types: resource bindings (role+resource+optional name) and
  operation bindings (role:run + action like backup, logs, impersonate)
- Name-scoped resource bindings for per-instance access control
- Remove role from project members (all permissions via RBAC)
- Add users, groups, RBAC CRUD endpoints and CLI commands
- describe user/group shows all RBAC access (direct + inherited)
- create rbac supports --subject, --binding, --operation flags
- Backup/restore handles users, groups, RBAC definitions
- mcplocal project-based MCP endpoint discovery
- Full test coverage for all new functionality

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:05:19 +00:00

95 lines
2.4 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import type { PrismaClient } from '@prisma/client';
import bcrypt from 'bcrypt';
/** 30 days in milliseconds */
const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
export interface LoginResult {
token: string;
expiresAt: Date;
user: { id: string; email: string; role: string };
}
export class AuthenticationError extends Error {
readonly statusCode = 401;
constructor(message: string) {
super(message);
this.name = 'AuthenticationError';
}
}
export class AuthService {
constructor(private readonly prisma: PrismaClient) {}
async login(email: string, password: string): Promise<LoginResult> {
const user = await this.prisma.user.findUnique({ where: { email } });
if (user === null) {
throw new AuthenticationError('Invalid email or password');
}
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
throw new AuthenticationError('Invalid email or password');
}
const token = randomUUID();
const expiresAt = new Date(Date.now() + SESSION_TTL_MS);
await this.prisma.session.create({
data: {
token,
userId: user.id,
expiresAt,
},
});
return {
token,
expiresAt,
user: { id: user.id, email: user.email, role: user.role },
};
}
async logout(token: string): Promise<void> {
// Delete the session by token; ignore if already deleted
await this.prisma.session.deleteMany({ where: { token } });
}
async findSession(token: string): Promise<{ userId: string; expiresAt: Date } | null> {
const session = await this.prisma.session.findUnique({ where: { token } });
if (session === null) {
return null;
}
return { userId: session.userId, expiresAt: session.expiresAt };
}
/**
* Create a session for a user by email without requiring their password.
* Used for admin impersonation.
*/
async impersonate(email: string): Promise<LoginResult> {
const user = await this.prisma.user.findUnique({ where: { email } });
if (user === null) {
throw new AuthenticationError('User not found');
}
const token = randomUUID();
const expiresAt = new Date(Date.now() + SESSION_TTL_MS);
await this.prisma.session.create({
data: {
token,
userId: user.id,
expiresAt,
},
});
return {
token,
expiresAt,
user: { id: user.id, email: user.email, role: user.role },
};
}
}