mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(audit-log): add persistent audit log system with comprehensive route instrumentation (#3242)
* feat(audit-log): add persistent audit log system with comprehensive route instrumentation
* fix(audit-log): address PR review — nullable workspaceId, enum usage, remove redundant queries
- Make audit_log.workspace_id nullable with ON DELETE SET NULL (logs survive workspace/user deletion)
- Make audit_log.actor_id nullable with ON DELETE SET NULL
- Replace all 53 routes' string literal action/resourceType with AuditAction.X and AuditResourceType.X enums
- Fix empty workspaceId ('') → null for OAuth, form, and org routes to avoid FK violations
- Remove redundant DB queries in chat manage route (use checkChatAccess return data)
- Fix organization routes to pass workspaceId: null instead of organizationId
* fix(audit-log): replace remaining workspaceId '' fallbacks with null
* fix(audit-log): credential-set org IDs, workspace deletion FK, actorId fallback, string literal action
* reran migrations
* fix(mcp,audit): tighten env var domain bypass, add post-resolution check, form workspaceId
- Only bypass MCP domain check when env var is in hostname/authority, not path/query
- Add post-resolution validateMcpDomain call in test-connection endpoint
- Match client-side isDomainAllowed to same hostname-only bypass logic
- Return workspaceId from checkFormAccess, use in form audit logs
- Add 49 comprehensive domain-check tests covering all edge cases
* fix(mcp): stateful regex lastIndex bug, RFC 3986 authority parsing
- Remove /g flag from module-level ENV_VAR_PATTERN to avoid lastIndex state
- Create fresh regex instances per call in server-side hasEnvVarInHostname
- Fix authority extraction to terminate at /, ?, or # per RFC 3986
- Prevents bypass via https://evil.com?token={{SECRET}} (no path)
- Add test cases for query-only and fragment-only env var URLs (53 total)
* fix(audit-log): try/catch for never-throw contract, accept null actorName/Email, fix misleading action
- Wrap recordAudit body in try/catch so nanoid() or header extraction can't throw
- Accept string | null for actorName and actorEmail (session.user.name can be null)
- Normalize null -> undefined before insert to match DB column types
- Fix org members route: ORG_MEMBER_ADDED -> ORG_INVITATION_CREATED (sends invite, not adds member)
* improvement(audit-log): add resource names and specific invitation actions
* fix(audit-log): use validated chat record, add mock sync tests
This commit is contained in:
@@ -45,6 +45,7 @@ export * from './assertions'
|
||||
export * from './builders'
|
||||
export * from './factories'
|
||||
export {
|
||||
auditMock,
|
||||
clearRedisMocks,
|
||||
createEnvMock,
|
||||
createMockDb,
|
||||
|
||||
108
packages/testing/src/mocks/audit.mock.ts
Normal file
108
packages/testing/src/mocks/audit.mock.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Mock module for @/lib/audit/log.
|
||||
* Use with vi.mock() to replace the real audit logger in tests.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/audit/log', () => auditMock)
|
||||
* ```
|
||||
*/
|
||||
export const auditMock = {
|
||||
recordAudit: vi.fn(),
|
||||
AuditAction: {
|
||||
API_KEY_CREATED: 'api_key.created',
|
||||
API_KEY_UPDATED: 'api_key.updated',
|
||||
API_KEY_REVOKED: 'api_key.revoked',
|
||||
PERSONAL_API_KEY_CREATED: 'personal_api_key.created',
|
||||
PERSONAL_API_KEY_REVOKED: 'personal_api_key.revoked',
|
||||
BYOK_KEY_CREATED: 'byok_key.created',
|
||||
BYOK_KEY_DELETED: 'byok_key.deleted',
|
||||
CHAT_DEPLOYED: 'chat.deployed',
|
||||
CHAT_UPDATED: 'chat.updated',
|
||||
CHAT_DELETED: 'chat.deleted',
|
||||
CREDENTIAL_SET_CREATED: 'credential_set.created',
|
||||
CREDENTIAL_SET_UPDATED: 'credential_set.updated',
|
||||
CREDENTIAL_SET_DELETED: 'credential_set.deleted',
|
||||
CREDENTIAL_SET_MEMBER_REMOVED: 'credential_set_member.removed',
|
||||
CREDENTIAL_SET_INVITATION_CREATED: 'credential_set_invitation.created',
|
||||
CREDENTIAL_SET_INVITATION_REVOKED: 'credential_set_invitation.revoked',
|
||||
DOCUMENT_UPLOADED: 'document.uploaded',
|
||||
DOCUMENT_UPDATED: 'document.updated',
|
||||
DOCUMENT_DELETED: 'document.deleted',
|
||||
ENVIRONMENT_UPDATED: 'environment.updated',
|
||||
FILE_UPLOADED: 'file.uploaded',
|
||||
FILE_DELETED: 'file.deleted',
|
||||
FOLDER_CREATED: 'folder.created',
|
||||
FOLDER_DELETED: 'folder.deleted',
|
||||
FOLDER_DUPLICATED: 'folder.duplicated',
|
||||
FORM_CREATED: 'form.created',
|
||||
FORM_UPDATED: 'form.updated',
|
||||
FORM_DELETED: 'form.deleted',
|
||||
INVITATION_ACCEPTED: 'invitation.accepted',
|
||||
INVITATION_REVOKED: 'invitation.revoked',
|
||||
KNOWLEDGE_BASE_CREATED: 'knowledge_base.created',
|
||||
KNOWLEDGE_BASE_UPDATED: 'knowledge_base.updated',
|
||||
KNOWLEDGE_BASE_DELETED: 'knowledge_base.deleted',
|
||||
MCP_SERVER_ADDED: 'mcp_server.added',
|
||||
MCP_SERVER_UPDATED: 'mcp_server.updated',
|
||||
MCP_SERVER_REMOVED: 'mcp_server.removed',
|
||||
MEMBER_INVITED: 'member.invited',
|
||||
MEMBER_REMOVED: 'member.removed',
|
||||
MEMBER_ROLE_CHANGED: 'member.role_changed',
|
||||
NOTIFICATION_CREATED: 'notification.created',
|
||||
NOTIFICATION_UPDATED: 'notification.updated',
|
||||
NOTIFICATION_DELETED: 'notification.deleted',
|
||||
OAUTH_DISCONNECTED: 'oauth.disconnected',
|
||||
ORGANIZATION_CREATED: 'organization.created',
|
||||
ORGANIZATION_UPDATED: 'organization.updated',
|
||||
ORG_MEMBER_ADDED: 'org_member.added',
|
||||
ORG_MEMBER_REMOVED: 'org_member.removed',
|
||||
ORG_MEMBER_ROLE_CHANGED: 'org_member.role_changed',
|
||||
ORG_INVITATION_CREATED: 'org_invitation.created',
|
||||
ORG_INVITATION_ACCEPTED: 'org_invitation.accepted',
|
||||
ORG_INVITATION_REJECTED: 'org_invitation.rejected',
|
||||
ORG_INVITATION_CANCELLED: 'org_invitation.cancelled',
|
||||
ORG_INVITATION_REVOKED: 'org_invitation.revoked',
|
||||
PERMISSION_GROUP_CREATED: 'permission_group.created',
|
||||
PERMISSION_GROUP_UPDATED: 'permission_group.updated',
|
||||
PERMISSION_GROUP_DELETED: 'permission_group.deleted',
|
||||
PERMISSION_GROUP_MEMBER_ADDED: 'permission_group_member.added',
|
||||
PERMISSION_GROUP_MEMBER_REMOVED: 'permission_group_member.removed',
|
||||
SCHEDULE_UPDATED: 'schedule.updated',
|
||||
WEBHOOK_CREATED: 'webhook.created',
|
||||
WEBHOOK_DELETED: 'webhook.deleted',
|
||||
WORKFLOW_CREATED: 'workflow.created',
|
||||
WORKFLOW_DELETED: 'workflow.deleted',
|
||||
WORKFLOW_DEPLOYED: 'workflow.deployed',
|
||||
WORKFLOW_UNDEPLOYED: 'workflow.undeployed',
|
||||
WORKFLOW_DUPLICATED: 'workflow.duplicated',
|
||||
WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
|
||||
WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',
|
||||
WORKSPACE_CREATED: 'workspace.created',
|
||||
WORKSPACE_DELETED: 'workspace.deleted',
|
||||
WORKSPACE_DUPLICATED: 'workspace.duplicated',
|
||||
},
|
||||
AuditResourceType: {
|
||||
API_KEY: 'api_key',
|
||||
BYOK_KEY: 'byok_key',
|
||||
CHAT: 'chat',
|
||||
CREDENTIAL_SET: 'credential_set',
|
||||
DOCUMENT: 'document',
|
||||
ENVIRONMENT: 'environment',
|
||||
FILE: 'file',
|
||||
FOLDER: 'folder',
|
||||
FORM: 'form',
|
||||
KNOWLEDGE_BASE: 'knowledge_base',
|
||||
MCP_SERVER: 'mcp_server',
|
||||
NOTIFICATION: 'notification',
|
||||
OAUTH: 'oauth',
|
||||
ORGANIZATION: 'organization',
|
||||
PERMISSION_GROUP: 'permission_group',
|
||||
SCHEDULE: 'schedule',
|
||||
WEBHOOK: 'webhook',
|
||||
WORKFLOW: 'workflow',
|
||||
WORKSPACE: 'workspace',
|
||||
},
|
||||
}
|
||||
@@ -24,6 +24,8 @@ export {
|
||||
mockKnowledgeSchemas,
|
||||
setupCommonApiMocks,
|
||||
} from './api.mock'
|
||||
// Audit mocks
|
||||
export { auditMock } from './audit.mock'
|
||||
// Auth mocks
|
||||
export {
|
||||
defaultMockUser,
|
||||
|
||||
Reference in New Issue
Block a user