mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-07 00:02:04 +00:00
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
|
|
/**
|
||
|
|
* Reads the hard-revocation deny-list the pre-token Lambda consults at mint time.
|
||
|
|
*
|
||
|
|
* The group-sync path (Google → Cognito groups, 5-min cadence) is the PRIMARY
|
||
|
|
* entitlement control; this deny-list is a fast-kill OVERLAY for incident response
|
||
|
|
* ("revoke this compromised `sub` now") that does not wait for the next sync. An
|
||
|
|
* entry keyed by the user's `sub` means: suppress every tier scope on this mint,
|
||
|
|
* dropping the principal to no MCP access until the (TTL'd) entry expires.
|
||
|
|
*
|
||
|
|
* Failure posture is deliberately fail-OPEN: a deny-list read error resolves to
|
||
|
|
* "not denied" (logged loudly for a metric-filter alarm) rather than denying, so
|
||
|
|
* a DynamoDB blip cannot lock every principal out of token issuance. The primary
|
||
|
|
* group-membership control — and its own fail-CLOSED 30-min freshness window —
|
||
|
|
* still governs entitlement. (design.md §2.3; security-review C2.)
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
||
|
|
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
|
||
|
|
|
||
|
|
let cached: DynamoDBDocumentClient | undefined;
|
||
|
|
|
||
|
|
function doc(): DynamoDBDocumentClient {
|
||
|
|
cached ??= DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
||
|
|
return cached;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* True iff `sub` has an active deny-list entry. A read failure resolves to
|
||
|
|
* `false` (fail OPEN — see module note) after a structured warn log so the
|
||
|
|
* outage is observable. An empty/absent `sub` is never denied.
|
||
|
|
*/
|
||
|
|
export async function isSubDenied(
|
||
|
|
tableName: string,
|
||
|
|
sub: string | undefined,
|
||
|
|
client: DynamoDBDocumentClient = doc(),
|
||
|
|
): Promise<boolean> {
|
||
|
|
if (!sub) return false;
|
||
|
|
try {
|
||
|
|
const res = await client.send(new GetCommand({ TableName: tableName, Key: { sub } }));
|
||
|
|
return res.Item !== undefined;
|
||
|
|
} catch (err) {
|
||
|
|
console.warn(
|
||
|
|
JSON.stringify({
|
||
|
|
event: 'deny_list_read_failed',
|
||
|
|
reason: err instanceof Error ? err.message : 'unknown',
|
||
|
|
failedOpen: true,
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|