Fix/mcp fixes and bugs (#518)
* feat(voucher): add create voucher and correct entry previews; update commit methods * feat: add support for pending operations in API key scopes and OAuth client management - Introduced new API key scopes for reading and approving pending operations. - Updated the scope groups to include pending operations. - Added new tools for listing and managing pending operations. - Implemented OAuth client registration and revocation endpoints. - Created a UI panel for managing OAuth clients, including registration and revocation. - Added tests for pending operations tools and OAuth allowlist functionality. - Implemented a database migration for OAuth client registrations with appropriate policies and constraints. * feat: Implement OAuth client registration rate limiting and enhance security measures - Added IP-based rate limiting to the OAuth client registration endpoint to prevent enumeration attacks. - Introduced a service-role client for allowlist lookups, ensuring trust boundaries are maintained. - Updated error responses to be uniform across different types of redirect URI validation failures. - Enhanced tests to reflect changes in OAuth scope handling, ensuring fallback to read-only scopes when no scopes are provided. - Improved handling of high-risk pending operations, requiring explicit confirmation for approvals. - Added audit logging for OAuth client revocations and pending operation approvals/rejections to maintain a security audit trail. - Refactored API key scope management to include default read-only scopes for OAuth-issued keys and added segregation-of-duties checks.
This commit is contained in:
+78
-7
@@ -27,6 +27,8 @@ export const API_KEY_SCOPES = {
|
||||
'documents:read': { label: 'Dokument — läs', description: 'Lista och hämta dokumentbilagor' },
|
||||
'documents:write': { label: 'Dokument — skriv', description: 'Ladda upp och koppla dokument till verifikationer' },
|
||||
'compliance:read': { label: 'Compliance — läs', description: 'Pre-flight-kontroller: momsstängning, bokslutsberedskap, voucher-gap, IB/UB-kontinuitet' },
|
||||
'pending_operations:read': { label: 'Stagade operationer — läs', description: 'Lista pending_operations (staged writes awaiting approval)' },
|
||||
'pending_operations:approve': { label: 'Stagade operationer — godkänn', description: 'Godkänn eller avvisa stagade operationer via API/MCP — agenten ersätter web-UI:s granskning' },
|
||||
} as const
|
||||
|
||||
export type ApiKeyScope = keyof typeof API_KEY_SCOPES
|
||||
@@ -42,15 +44,79 @@ export const DEFAULT_SCOPES: ApiKeyScope[] = [
|
||||
'reports:read',
|
||||
]
|
||||
|
||||
/**
|
||||
* Read-only fallback granted to OAuth-issued keys when the client did not
|
||||
* pass an explicit `scope` parameter at /authorize. Per GDPR Art. 25(2)
|
||||
* (data protection by default), the silent fallback must never include
|
||||
* destructive scopes (*:write, pending_operations:approve, bookkeeping:write).
|
||||
* Destructive scopes must be requested explicitly by the client and consented
|
||||
* to by the user.
|
||||
*/
|
||||
export const DEFAULT_OAUTH_SCOPES: ApiKeyScope[] = [
|
||||
'transactions:read',
|
||||
'customers:read',
|
||||
'invoices:read',
|
||||
'suppliers:read',
|
||||
'reports:read',
|
||||
'companies:read',
|
||||
'events:read',
|
||||
'operations:read',
|
||||
'documents:read',
|
||||
'compliance:read',
|
||||
'payroll:read',
|
||||
'pending_operations:read',
|
||||
]
|
||||
|
||||
/**
|
||||
* Scopes advertised in the RFC 8414 authorization-server metadata document
|
||||
* (/.well-known/oauth-authorization-server). Restricted to the same set that
|
||||
* /authorize will grant by default — destructive scopes still work when
|
||||
* requested explicitly, they just aren't enumerated for unauthenticated
|
||||
* callers (defense-in-depth against scope-escalation reconnaissance).
|
||||
*/
|
||||
export const PUBLIC_OAUTH_METADATA_SCOPES: ApiKeyScope[] = [...DEFAULT_OAUTH_SCOPES]
|
||||
|
||||
/**
|
||||
* Scopes that allow staging a pending_operation. Used to detect a
|
||||
* segregation-of-duties conflict when paired with `pending_operations:approve`
|
||||
* on the same API key (ISO 27001:2022 A.5.3, SOC 2 CC6.1).
|
||||
*/
|
||||
export const STAGING_SCOPES: ApiKeyScope[] = [
|
||||
'transactions:write',
|
||||
'customers:write',
|
||||
'invoices:write',
|
||||
'suppliers:write',
|
||||
'bookkeeping:write',
|
||||
'payroll:write',
|
||||
'documents:write',
|
||||
]
|
||||
|
||||
/**
|
||||
* Detect a segregation-of-duties conflict between staging and approval scopes
|
||||
* on the same key. Returns the offending staging scope, or null when the
|
||||
* combination is clean. Callers may choose to block, warn, or record an
|
||||
* acknowledged risk acceptance.
|
||||
*
|
||||
* Granting both stage+approve to the same actor lets an automated agent both
|
||||
* stage AND commit financial postings without a human-in-the-loop review,
|
||||
* which is the explicit control surface for BFNAR 2013:2 (behandlingshistorik)
|
||||
* and BFL 5 kap 5§ traceability requirements.
|
||||
*/
|
||||
export function findStageApproveConflict(scopes: ApiKeyScope[]): ApiKeyScope | null {
|
||||
if (!scopes.includes('pending_operations:approve')) return null
|
||||
return scopes.find((s) => STAGING_SCOPES.includes(s)) ?? null
|
||||
}
|
||||
|
||||
/** Scope domain groups for UI rendering */
|
||||
export const SCOPE_GROUPS = [
|
||||
{ domain: 'transactions', label: 'Transaktioner', read: 'transactions:read' as const, write: 'transactions:write' as const },
|
||||
{ domain: 'customers', label: 'Kunder', read: 'customers:read' as const, write: 'customers:write' as const },
|
||||
{ domain: 'invoices', label: 'Fakturor', read: 'invoices:read' as const, write: 'invoices:write' as const },
|
||||
{ domain: 'suppliers', label: 'Leverantörer', read: 'suppliers:read' as const, write: 'suppliers:write' as const },
|
||||
{ domain: 'reports', label: 'Rapporter', read: 'reports:read' as const, write: null },
|
||||
{ domain: 'bookkeeping', label: 'Bokföring', read: null, write: 'bookkeeping:write' as const },
|
||||
{ domain: 'payroll', label: 'Löner', read: 'payroll:read' as const, write: 'payroll:write' as const },
|
||||
{ domain: 'transactions', label: 'Transaktioner', read: 'transactions:read' as const, write: 'transactions:write' as const },
|
||||
{ domain: 'customers', label: 'Kunder', read: 'customers:read' as const, write: 'customers:write' as const },
|
||||
{ domain: 'invoices', label: 'Fakturor', read: 'invoices:read' as const, write: 'invoices:write' as const },
|
||||
{ domain: 'suppliers', label: 'Leverantörer', read: 'suppliers:read' as const, write: 'suppliers:write' as const },
|
||||
{ domain: 'reports', label: 'Rapporter', read: 'reports:read' as const, write: null },
|
||||
{ domain: 'bookkeeping', label: 'Bokföring', read: null, write: 'bookkeeping:write' as const },
|
||||
{ domain: 'payroll', label: 'Löner', read: 'payroll:read' as const, write: 'payroll:write' as const },
|
||||
{ domain: 'pending_operations', label: 'Stagade operationer', read: 'pending_operations:read' as const, write: 'pending_operations:approve' as const },
|
||||
] as const
|
||||
|
||||
/** Map MCP tool name → required scope. Tools omitted from this map are available to any authenticated key (e.g. discovery/search/skill loading). */
|
||||
@@ -126,6 +192,7 @@ export const TOOL_SCOPE_MAP: Record<string, ApiKeyScope> = {
|
||||
gnubok_approve_supplier_invoice: 'suppliers:write',
|
||||
gnubok_credit_supplier_invoice: 'suppliers:write',
|
||||
gnubok_create_supplier_invoice_from_inbox: 'suppliers:write',
|
||||
gnubok_set_inbox_extracted_data: 'suppliers:write',
|
||||
// Invoice conversion + crediting
|
||||
gnubok_convert_invoice: 'invoices:write',
|
||||
gnubok_credit_invoice: 'invoices:write',
|
||||
@@ -133,6 +200,10 @@ export const TOOL_SCOPE_MAP: Record<string, ApiKeyScope> = {
|
||||
gnubok_create_voucher: 'bookkeeping:write',
|
||||
gnubok_correct_entry: 'bookkeeping:write',
|
||||
gnubok_reverse_journal_entry: 'bookkeeping:write',
|
||||
// Pending operations approval (mirrors the /pending web UI)
|
||||
gnubok_list_pending_operations: 'pending_operations:read',
|
||||
gnubok_approve_pending_operation: 'pending_operations:approve',
|
||||
gnubok_reject_pending_operation: 'pending_operations:approve',
|
||||
}
|
||||
|
||||
export function validateScopes(scopes: unknown): ApiKeyScope[] | null {
|
||||
|
||||
Reference in New Issue
Block a user