feat: granular RBAC with resource/operation bindings, users, groups
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

- 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>
This commit is contained in:
Michal
2026-02-23 11:05:19 +00:00
parent a6b5e24a8d
commit dcda93d179
67 changed files with 7256 additions and 498 deletions

View File

@@ -11,10 +11,17 @@ function makeServer(overrides: Partial<McpServer> = {}): McpServer {
dockerImage: null,
transport: 'STDIO',
repositoryUrl: null,
externalUrl: null,
command: null,
containerPort: null,
replicas: 1,
env: [],
healthCheck: null,
version: 1,
createdAt: new Date(),
updatedAt: new Date(),
templateName: null,
templateVersion: null,
...overrides,
};
}
@@ -25,7 +32,7 @@ describe('generateMcpConfig', () => {
expect(result).toEqual({ mcpServers: {} });
});
it('generates config for a single server', () => {
it('generates config for a single STDIO server', () => {
const result = generateMcpConfig([
{ server: makeServer(), resolvedEnv: {} },
]);
@@ -34,7 +41,7 @@ describe('generateMcpConfig', () => {
expect(result.mcpServers['slack']?.args).toEqual(['-y', '@anthropic/slack-mcp']);
});
it('includes resolved env when present', () => {
it('includes resolved env when present for STDIO server', () => {
const result = generateMcpConfig([
{ server: makeServer(), resolvedEnv: { SLACK_TEAM_ID: 'T123' } },
]);
@@ -67,4 +74,35 @@ describe('generateMcpConfig', () => {
]);
expect(result.mcpServers['slack']?.args).toEqual(['-y', 'slack']);
});
it('generates URL-based config for SSE servers', () => {
const server = makeServer({ name: 'sse-server', transport: 'SSE' });
const result = generateMcpConfig([
{ server, resolvedEnv: { TOKEN: 'abc' } },
]);
const config = result.mcpServers['sse-server'];
expect(config?.url).toBe('http://localhost:3100/api/v1/mcp/proxy/sse-server');
expect(config?.command).toBeUndefined();
expect(config?.args).toBeUndefined();
expect(config?.env).toBeUndefined();
});
it('generates URL-based config for STREAMABLE_HTTP servers', () => {
const server = makeServer({ name: 'stream-server', transport: 'STREAMABLE_HTTP' });
const result = generateMcpConfig([
{ server, resolvedEnv: {} },
]);
const config = result.mcpServers['stream-server'];
expect(config?.url).toBe('http://localhost:3100/api/v1/mcp/proxy/stream-server');
expect(config?.command).toBeUndefined();
});
it('mixes STDIO and SSE servers correctly', () => {
const result = generateMcpConfig([
{ server: makeServer({ name: 'stdio-srv', transport: 'STDIO' }), resolvedEnv: {} },
{ server: makeServer({ name: 'sse-srv', transport: 'SSE' }), resolvedEnv: {} },
]);
expect(result.mcpServers['stdio-srv']?.command).toBe('npx');
expect(result.mcpServers['sse-srv']?.url).toBeDefined();
});
});