feat: mcpctl mcptoken verbs + mcpd auth dispatch + audit plumbing
Adds the end-to-end CLI surface for McpTokens and the mcpd auth dispatch
that recognizes them.
mcpd auth middleware:
- Dispatch on the `mcpctl_pat_` bearer prefix. McpToken bearers resolve
through a new `findMcpToken(hash)` dep, populating `request.mcpToken`
and `request.userId = ownerId`. Everything else follows the existing
session path.
- Returns 401 for revoked / expired / unknown tokens.
- Global RBAC hook now threads `mcpTokenSha` into `canAccess` /
`canRunOperation` / `getAllowedScope`, and enforces a hard
project-scope check: a McpToken principal can only hit
`/api/v1/projects/<its-project>/...`.
CLI verbs:
- `mcpctl create mcptoken <name> -p <proj> [--rbac empty|clone]
[--bind role:view,resource:servers] [--ttl 30d|never|ISO]
[--description ...] [--force]` — returns the raw token once.
- `mcpctl get mcptokens [-p <proj>]` — table with
NAME/PROJECT/PREFIX/CREATED/LAST USED/EXPIRES/STATUS.
- `mcpctl get mcptoken <name> -p <proj>` and
`mcpctl describe mcptoken <name> -p <proj>` — describe surfaces the
auto-created RBAC bindings.
- `mcpctl delete mcptoken <name> -p <proj>`.
- `apply -f` support with `kind: mcptoken`. Tokens are immutable, so
apply creates if missing and skips if the name is already active.
Audit plumbing:
- `AuditEvent` / collector now carry optional `tokenName` / `tokenSha`.
`setSessionMcpToken` sits alongside `setSessionUserName`; both feed a
per-session principal map used at emit time.
- `AuditEventService` query accepts `tokenName` / `tokenSha` filters.
- Console `AuditEvent` type carries the new fields so a follow-up can
add a TOKEN column.
Completions regenerated. 1764/1764 tests pass workspace-wide.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,11 +10,17 @@ import type { McpdClient } from '../http/mcpd-client.js';
|
||||
const BATCH_SIZE = 50;
|
||||
const FLUSH_INTERVAL_MS = 5_000;
|
||||
|
||||
interface SessionPrincipal {
|
||||
userName?: string;
|
||||
tokenName?: string;
|
||||
tokenSha?: string;
|
||||
}
|
||||
|
||||
export class AuditCollector {
|
||||
private queue: AuditEvent[] = [];
|
||||
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private flushing = false;
|
||||
private sessionUserNames = new Map<string, string>();
|
||||
private sessionPrincipals = new Map<string, SessionPrincipal>();
|
||||
|
||||
constructor(
|
||||
private readonly mcpdClient: McpdClient,
|
||||
@@ -25,15 +31,26 @@ export class AuditCollector {
|
||||
|
||||
/** Register a userName for a session. All future events for this session auto-fill it. */
|
||||
setSessionUserName(sessionId: string, userName: string): void {
|
||||
this.sessionUserNames.set(sessionId, userName);
|
||||
const existing = this.sessionPrincipals.get(sessionId) ?? {};
|
||||
this.sessionPrincipals.set(sessionId, { ...existing, userName });
|
||||
}
|
||||
|
||||
/** Queue an audit event. Auto-fills projectName and userName (from session map). */
|
||||
/** Register McpToken identity for a session (HTTP-mode authenticated requests). */
|
||||
setSessionMcpToken(sessionId: string, token: { tokenName: string; tokenSha: string }): void {
|
||||
const existing = this.sessionPrincipals.get(sessionId) ?? {};
|
||||
this.sessionPrincipals.set(sessionId, { ...existing, tokenName: token.tokenName, tokenSha: token.tokenSha });
|
||||
}
|
||||
|
||||
/** Queue an audit event. Auto-fills projectName, userName, tokenName, and tokenSha. */
|
||||
emit(event: Omit<AuditEvent, 'projectName'>): void {
|
||||
const enriched: AuditEvent = { ...event, projectName: this.projectName };
|
||||
if (!enriched.userName && enriched.sessionId) {
|
||||
const name = this.sessionUserNames.get(enriched.sessionId);
|
||||
if (name) enriched.userName = name;
|
||||
if (enriched.sessionId) {
|
||||
const principal = this.sessionPrincipals.get(enriched.sessionId);
|
||||
if (principal) {
|
||||
if (!enriched.userName && principal.userName) enriched.userName = principal.userName;
|
||||
if (!enriched.tokenName && principal.tokenName) enriched.tokenName = principal.tokenName;
|
||||
if (!enriched.tokenSha && principal.tokenSha) enriched.tokenSha = principal.tokenSha;
|
||||
}
|
||||
}
|
||||
this.queue.push(enriched);
|
||||
if (this.queue.length >= BATCH_SIZE) {
|
||||
|
||||
@@ -32,5 +32,9 @@ export interface AuditEvent {
|
||||
correlationId?: string;
|
||||
parentEventId?: string;
|
||||
userName?: string;
|
||||
/** Set when the session authenticated via an McpToken (HTTP-mode mcplocal). */
|
||||
tokenName?: string;
|
||||
/** SHA-256 hash of the McpToken that made the request. */
|
||||
tokenSha?: string;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user