feat: remove ProjectMember, add expose RBAC role, attach/detach-server commands
- Remove ProjectMember model entirely (RBAC manages project access) - Add 'expose' RBAC role for /mcp-config endpoint access (edit implies expose) - Rename CLI flags: --llm-provider → --proxy-mode-llm-provider, --llm-model → --proxy-mode-llm-model - Add attach-server / detach-server CLI commands (mcpctl --project NAME attach-server SERVER) - Add POST/DELETE /api/v1/projects/:id/servers endpoints for server attach/detach - Remove members from backup/restore, apply, get, describe - Prisma migration to drop ProjectMember table Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -921,4 +921,92 @@ describe('RbacService', () => {
|
||||
expect(await svc.canAccess('user-1', 'edit', 'servers')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expose role', () => {
|
||||
it('grants expose access with expose role binding', async () => {
|
||||
const repo = mockRepo([
|
||||
makeDef({
|
||||
roleBindings: [{ role: 'expose', resource: 'projects' }],
|
||||
}),
|
||||
]);
|
||||
const prisma = mockPrisma({
|
||||
user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
|
||||
groupMember: { findMany: vi.fn(async () => []) },
|
||||
});
|
||||
const service = new RbacService(repo, prisma);
|
||||
expect(await service.canAccess('user-1', 'expose', 'projects')).toBe(true);
|
||||
});
|
||||
|
||||
it('grants expose access with edit role binding (edit includes expose)', async () => {
|
||||
const repo = mockRepo([
|
||||
makeDef({
|
||||
roleBindings: [{ role: 'edit', resource: 'projects' }],
|
||||
}),
|
||||
]);
|
||||
const prisma = mockPrisma({
|
||||
user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
|
||||
groupMember: { findMany: vi.fn(async () => []) },
|
||||
});
|
||||
const service = new RbacService(repo, prisma);
|
||||
expect(await service.canAccess('user-1', 'expose', 'projects')).toBe(true);
|
||||
});
|
||||
|
||||
it('denies expose access with view role binding', async () => {
|
||||
const repo = mockRepo([
|
||||
makeDef({
|
||||
roleBindings: [{ role: 'view', resource: 'projects' }],
|
||||
}),
|
||||
]);
|
||||
const prisma = mockPrisma({
|
||||
user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
|
||||
groupMember: { findMany: vi.fn(async () => []) },
|
||||
});
|
||||
const service = new RbacService(repo, prisma);
|
||||
expect(await service.canAccess('user-1', 'expose', 'projects')).toBe(false);
|
||||
});
|
||||
|
||||
it('expose role also grants view access', async () => {
|
||||
const repo = mockRepo([
|
||||
makeDef({
|
||||
roleBindings: [{ role: 'expose', resource: 'projects' }],
|
||||
}),
|
||||
]);
|
||||
const prisma = mockPrisma({
|
||||
user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
|
||||
groupMember: { findMany: vi.fn(async () => []) },
|
||||
});
|
||||
const service = new RbacService(repo, prisma);
|
||||
expect(await service.canAccess('user-1', 'view', 'projects')).toBe(true);
|
||||
});
|
||||
|
||||
it('expose role with name-scoped binding', async () => {
|
||||
const repo = mockRepo([
|
||||
makeDef({
|
||||
roleBindings: [{ role: 'expose', resource: 'projects', name: 'my-project' }],
|
||||
}),
|
||||
]);
|
||||
const prisma = mockPrisma({
|
||||
user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
|
||||
groupMember: { findMany: vi.fn(async () => []) },
|
||||
});
|
||||
const service = new RbacService(repo, prisma);
|
||||
expect(await service.canAccess('user-1', 'expose', 'projects', 'my-project')).toBe(true);
|
||||
expect(await service.canAccess('user-1', 'expose', 'projects', 'other-project')).toBe(false);
|
||||
});
|
||||
|
||||
it('getAllowedScope with expose role grants view scope', async () => {
|
||||
const repo = mockRepo([
|
||||
makeDef({
|
||||
roleBindings: [{ role: 'expose', resource: 'projects' }],
|
||||
}),
|
||||
]);
|
||||
const prisma = mockPrisma({
|
||||
user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
|
||||
groupMember: { findMany: vi.fn(async () => []) },
|
||||
});
|
||||
const service = new RbacService(repo, prisma);
|
||||
const scope = await service.getAllowedScope('user-1', 'view', 'projects');
|
||||
expect(scope.wildcard).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user