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:
@@ -37,7 +37,6 @@ const mockProjects = [
|
||||
id: 'proj1', name: 'my-project', description: 'Test project', proxyMode: 'direct', llmProvider: null, llmModel: null,
|
||||
ownerId: 'user1', version: 1, createdAt: new Date(), updatedAt: new Date(),
|
||||
servers: [{ id: 'ps1', server: { id: 's1', name: 'github' } }],
|
||||
members: [{ id: 'pm1', user: { id: 'u1', email: 'alice@test.com', name: 'Alice' } }],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -91,11 +90,12 @@ function mockProjectRepo(): IProjectRepository {
|
||||
findAll: vi.fn(async () => [...mockProjects]),
|
||||
findById: vi.fn(async (id: string) => mockProjects.find((p) => p.id === id) ?? null),
|
||||
findByName: vi.fn(async () => null),
|
||||
create: vi.fn(async (data) => ({ id: 'new-proj', ...data, servers: [], members: [], version: 1, createdAt: new Date(), updatedAt: new Date() } as typeof mockProjects[0])),
|
||||
create: vi.fn(async (data) => ({ id: 'new-proj', ...data, servers: [], version: 1, createdAt: new Date(), updatedAt: new Date() } as typeof mockProjects[0])),
|
||||
update: vi.fn(async (id, data) => ({ ...mockProjects.find((p) => p.id === id)!, ...data })),
|
||||
delete: vi.fn(async () => {}),
|
||||
setServers: vi.fn(async () => {}),
|
||||
setMembers: vi.fn(async () => {}),
|
||||
addServer: vi.fn(async () => {}),
|
||||
removeServer: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,12 +214,11 @@ describe('BackupService', () => {
|
||||
expect(bundle.rbacBindings![0]!.subjects).toEqual([{ kind: 'User', name: 'alice@test.com' }]);
|
||||
});
|
||||
|
||||
it('includes enriched projects with server names and members', async () => {
|
||||
it('includes enriched projects with server names', async () => {
|
||||
const bundle = await backupService.createBackup();
|
||||
const proj = bundle.projects[0]!;
|
||||
expect(proj.proxyMode).toBe('direct');
|
||||
expect(proj.serverNames).toEqual(['github']);
|
||||
expect(proj.members).toEqual(['alice@test.com']);
|
||||
});
|
||||
|
||||
it('filters resources', async () => {
|
||||
@@ -406,7 +405,7 @@ describe('RestoreService', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('restores enriched projects with server and member linking', async () => {
|
||||
it('restores enriched projects with server linking', async () => {
|
||||
// Simulate servers exist (restored in prior step)
|
||||
(serverRepo.findByName as ReturnType<typeof vi.fn>).mockResolvedValue(null);
|
||||
// After server restore, we can find them
|
||||
@@ -419,14 +418,6 @@ describe('RestoreService', () => {
|
||||
return null;
|
||||
});
|
||||
|
||||
// Simulate users exist for member resolution
|
||||
let userCallCount = 0;
|
||||
(userRepo.findByEmail as ReturnType<typeof vi.fn>).mockImplementation(async (email: string) => {
|
||||
userCallCount++;
|
||||
if (userCallCount > 2 && email === 'alice@test.com') return { id: 'restored-u1', email };
|
||||
return null;
|
||||
});
|
||||
|
||||
const result = await restoreService.restore(fullBundle);
|
||||
|
||||
expect(result.projectsCreated).toBe(1);
|
||||
@@ -437,7 +428,6 @@ describe('RestoreService', () => {
|
||||
llmModel: 'gpt-4',
|
||||
}));
|
||||
expect(projectRepo.setServers).toHaveBeenCalled();
|
||||
expect(projectRepo.setMembers).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('restores old bundle without users/groups/rbac', async () => {
|
||||
@@ -551,7 +541,7 @@ describe('RestoreService', () => {
|
||||
(serverRepo.create as ReturnType<typeof vi.fn>).mockImplementation(async () => { callOrder.push('server'); return { id: 'srv' }; });
|
||||
(userRepo.create as ReturnType<typeof vi.fn>).mockImplementation(async () => { callOrder.push('user'); return { id: 'usr' }; });
|
||||
(groupRepo.create as ReturnType<typeof vi.fn>).mockImplementation(async () => { callOrder.push('group'); return { id: 'grp' }; });
|
||||
(projectRepo.create as ReturnType<typeof vi.fn>).mockImplementation(async () => { callOrder.push('project'); return { id: 'proj', servers: [], members: [] }; });
|
||||
(projectRepo.create as ReturnType<typeof vi.fn>).mockImplementation(async () => { callOrder.push('project'); return { id: 'proj', servers: [] }; });
|
||||
(rbacRepo.create as ReturnType<typeof vi.fn>).mockImplementation(async () => { callOrder.push('rbac'); return { id: 'rbac' }; });
|
||||
|
||||
await restoreService.restore(fullBundle);
|
||||
|
||||
Reference in New Issue
Block a user