@simonsbs/keylore 1.0.0-rc4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +64 -0
- package/LICENSE +176 -0
- package/NOTICE +5 -0
- package/README.md +424 -0
- package/bin/keylore-http.js +3 -0
- package/bin/keylore-stdio.js +3 -0
- package/data/auth-clients.json +54 -0
- package/data/catalog.json +53 -0
- package/data/policies.json +25 -0
- package/dist/adapters/adapter-registry.js +143 -0
- package/dist/adapters/aws-secrets-manager-adapter.js +99 -0
- package/dist/adapters/command-runner.js +17 -0
- package/dist/adapters/env-secret-adapter.js +42 -0
- package/dist/adapters/gcp-secret-manager-adapter.js +129 -0
- package/dist/adapters/local-secret-adapter.js +54 -0
- package/dist/adapters/onepassword-secret-adapter.js +83 -0
- package/dist/adapters/reference-utils.js +44 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/vault-secret-adapter.js +103 -0
- package/dist/app.js +132 -0
- package/dist/cli/args.js +51 -0
- package/dist/cli/run.js +483 -0
- package/dist/cli.js +18 -0
- package/dist/config.js +295 -0
- package/dist/domain/types.js +967 -0
- package/dist/http/admin-ui.js +3010 -0
- package/dist/http/server.js +1210 -0
- package/dist/index.js +40 -0
- package/dist/mcp/create-server.js +388 -0
- package/dist/mcp/stdio.js +7 -0
- package/dist/repositories/credential-repository.js +109 -0
- package/dist/repositories/interfaces.js +1 -0
- package/dist/repositories/json-file.js +20 -0
- package/dist/repositories/pg-access-token-repository.js +118 -0
- package/dist/repositories/pg-approval-repository.js +157 -0
- package/dist/repositories/pg-audit-log.js +62 -0
- package/dist/repositories/pg-auth-client-repository.js +98 -0
- package/dist/repositories/pg-authorization-code-repository.js +95 -0
- package/dist/repositories/pg-break-glass-repository.js +174 -0
- package/dist/repositories/pg-credential-repository.js +163 -0
- package/dist/repositories/pg-oauth-client-assertion-repository.js +25 -0
- package/dist/repositories/pg-policy-repository.js +62 -0
- package/dist/repositories/pg-refresh-token-repository.js +125 -0
- package/dist/repositories/pg-rotation-run-repository.js +127 -0
- package/dist/repositories/pg-tenant-repository.js +56 -0
- package/dist/repositories/policy-repository.js +24 -0
- package/dist/runtime/sandbox-runner.js +114 -0
- package/dist/services/access-fingerprint.js +13 -0
- package/dist/services/approval-service.js +148 -0
- package/dist/services/audit-log.js +38 -0
- package/dist/services/auth-context.js +43 -0
- package/dist/services/auth-secrets.js +14 -0
- package/dist/services/auth-service.js +784 -0
- package/dist/services/backup-service.js +610 -0
- package/dist/services/break-glass-service.js +207 -0
- package/dist/services/broker-service.js +557 -0
- package/dist/services/core-mode-service.js +154 -0
- package/dist/services/egress-policy.js +119 -0
- package/dist/services/local-secret-store.js +119 -0
- package/dist/services/maintenance-service.js +99 -0
- package/dist/services/notification-service.js +83 -0
- package/dist/services/policy-engine.js +85 -0
- package/dist/services/rate-limit-service.js +80 -0
- package/dist/services/rotation-service.js +271 -0
- package/dist/services/telemetry.js +149 -0
- package/dist/services/tenant-service.js +127 -0
- package/dist/services/trace-export-service.js +126 -0
- package/dist/services/trace-service.js +87 -0
- package/dist/storage/bootstrap.js +68 -0
- package/dist/storage/database.js +39 -0
- package/dist/storage/in-memory-database.js +40 -0
- package/dist/storage/migrations.js +27 -0
- package/migrations/001_init.sql +49 -0
- package/migrations/002_phase2_auth.sql +53 -0
- package/migrations/003_v05_operations.sql +9 -0
- package/migrations/004_v07_security.sql +28 -0
- package/migrations/005_v08_reviews.sql +11 -0
- package/migrations/006_v09_auth_trace_rotation.sql +51 -0
- package/migrations/007_v010_multi_tenant.sql +32 -0
- package/migrations/008_v011_auth_tenant_ops.sql +95 -0
- package/package.json +78 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
import { accessTokenRecordSchema, approvalRequestSchema, auditEventSchema, authClientRecordSchema, backupSummarySchema, breakGlassRequestSchema, credentialRecordSchema, policyFileSchema, refreshTokenRecordSchema, rotationRunSchema, tenantRecordSchema, } from "../domain/types.js";
|
|
4
|
+
const storedAuthClientSchema = authClientRecordSchema.extend({
|
|
5
|
+
secretHash: z.string().min(1).optional(),
|
|
6
|
+
secretSalt: z.string().min(1).optional(),
|
|
7
|
+
});
|
|
8
|
+
const backupEnvelopeSchema = z.object({
|
|
9
|
+
format: z.literal("keylore-logical-backup"),
|
|
10
|
+
version: z.number().int().positive(),
|
|
11
|
+
createdAt: z.string().datetime(),
|
|
12
|
+
sourceVersion: z.string().min(1),
|
|
13
|
+
tenants: z.array(tenantRecordSchema),
|
|
14
|
+
credentials: z.array(credentialRecordSchema),
|
|
15
|
+
policies: policyFileSchema,
|
|
16
|
+
authClients: z.array(storedAuthClientSchema),
|
|
17
|
+
accessTokens: z.array(accessTokenRecordSchema.extend({ tokenHash: z.string().min(1) })),
|
|
18
|
+
refreshTokens: z.array(refreshTokenRecordSchema.extend({ tokenHash: z.string().min(1) })),
|
|
19
|
+
approvals: z.array(approvalRequestSchema),
|
|
20
|
+
breakGlassRequests: z.array(breakGlassRequestSchema),
|
|
21
|
+
rotationRuns: z.array(rotationRunSchema).default([]),
|
|
22
|
+
auditEvents: z.array(auditEventSchema),
|
|
23
|
+
});
|
|
24
|
+
function toIso(value) {
|
|
25
|
+
if (value === null) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
29
|
+
}
|
|
30
|
+
function normalizeJwks(value) {
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
if (value && typeof value === "object" && "kty" in value) {
|
|
35
|
+
return [value];
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
function tenantOnly(records, tenantId) {
|
|
40
|
+
return tenantId ? records.filter((record) => record.tenantId === tenantId) : records;
|
|
41
|
+
}
|
|
42
|
+
export class BackupService {
|
|
43
|
+
database;
|
|
44
|
+
sourceVersion;
|
|
45
|
+
audit;
|
|
46
|
+
constructor(database, sourceVersion, audit) {
|
|
47
|
+
this.database = database;
|
|
48
|
+
this.sourceVersion = sourceVersion;
|
|
49
|
+
this.audit = audit;
|
|
50
|
+
}
|
|
51
|
+
summarizeBackup(backup) {
|
|
52
|
+
return backupSummarySchema.parse({
|
|
53
|
+
format: backup.format,
|
|
54
|
+
version: backup.version,
|
|
55
|
+
sourceVersion: backup.sourceVersion,
|
|
56
|
+
createdAt: backup.createdAt,
|
|
57
|
+
tenants: backup.tenants.length,
|
|
58
|
+
credentials: backup.credentials.length,
|
|
59
|
+
authClients: backup.authClients.length,
|
|
60
|
+
accessTokens: backup.accessTokens.length,
|
|
61
|
+
refreshTokens: backup.refreshTokens.length,
|
|
62
|
+
approvals: backup.approvals.length,
|
|
63
|
+
breakGlassRequests: backup.breakGlassRequests.length,
|
|
64
|
+
rotationRuns: backup.rotationRuns.length,
|
|
65
|
+
auditEvents: backup.auditEvents.length,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
parseBackupPayload(payload) {
|
|
69
|
+
return backupEnvelopeSchema.parse(payload);
|
|
70
|
+
}
|
|
71
|
+
filterBackupForTenant(backup, tenantId) {
|
|
72
|
+
if (!tenantId) {
|
|
73
|
+
return backup;
|
|
74
|
+
}
|
|
75
|
+
return backupEnvelopeSchema.parse({
|
|
76
|
+
...backup,
|
|
77
|
+
tenants: tenantOnly(backup.tenants, tenantId),
|
|
78
|
+
credentials: tenantOnly(backup.credentials, tenantId),
|
|
79
|
+
policies: {
|
|
80
|
+
...backup.policies,
|
|
81
|
+
rules: tenantOnly(backup.policies.rules, tenantId),
|
|
82
|
+
},
|
|
83
|
+
authClients: tenantOnly(backup.authClients, tenantId),
|
|
84
|
+
accessTokens: tenantOnly(backup.accessTokens, tenantId),
|
|
85
|
+
refreshTokens: tenantOnly(backup.refreshTokens, tenantId),
|
|
86
|
+
approvals: tenantOnly(backup.approvals, tenantId),
|
|
87
|
+
breakGlassRequests: tenantOnly(backup.breakGlassRequests, tenantId),
|
|
88
|
+
rotationRuns: tenantOnly(backup.rotationRuns, tenantId),
|
|
89
|
+
auditEvents: tenantOnly(backup.auditEvents, tenantId),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
assertTenantScopedBackup(backup, tenantId) {
|
|
93
|
+
if (!backup.tenants.some((tenant) => tenant.tenantId === tenantId)) {
|
|
94
|
+
throw new Error(`Tenant-scoped restore payload is missing tenant metadata: ${tenantId}`);
|
|
95
|
+
}
|
|
96
|
+
const mismatchedTenants = new Set();
|
|
97
|
+
const collect = (records) => {
|
|
98
|
+
for (const record of records) {
|
|
99
|
+
if (record.tenantId !== tenantId) {
|
|
100
|
+
mismatchedTenants.add(record.tenantId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
collect(backup.tenants);
|
|
105
|
+
collect(backup.credentials);
|
|
106
|
+
collect(backup.policies.rules);
|
|
107
|
+
collect(backup.authClients);
|
|
108
|
+
collect(backup.accessTokens);
|
|
109
|
+
collect(backup.refreshTokens);
|
|
110
|
+
collect(backup.approvals);
|
|
111
|
+
collect(backup.breakGlassRequests);
|
|
112
|
+
collect(backup.rotationRuns);
|
|
113
|
+
collect(backup.auditEvents);
|
|
114
|
+
if (mismatchedTenants.size > 0) {
|
|
115
|
+
throw new Error(`Tenant-scoped restore payload includes foreign tenant data: ${Array.from(mismatchedTenants).sort().join(", ")}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async exportBackup(actor) {
|
|
119
|
+
const [tenants, credentials, policies, authClients, accessTokens, refreshTokens, approvals, breakGlassRequests, rotationRuns, auditEvents] = await Promise.all([
|
|
120
|
+
this.database.query("SELECT * FROM tenants ORDER BY tenant_id"),
|
|
121
|
+
this.database.query("SELECT * FROM credentials ORDER BY id"),
|
|
122
|
+
this.database.query("SELECT * FROM policy_rules ORDER BY id"),
|
|
123
|
+
this.database.query("SELECT * FROM oauth_clients ORDER BY client_id"),
|
|
124
|
+
this.database.query("SELECT * FROM access_tokens ORDER BY created_at"),
|
|
125
|
+
this.database.query("SELECT * FROM refresh_tokens ORDER BY created_at"),
|
|
126
|
+
this.database.query("SELECT * FROM approval_requests ORDER BY created_at"),
|
|
127
|
+
this.database.query("SELECT * FROM break_glass_requests ORDER BY created_at"),
|
|
128
|
+
this.database.query("SELECT * FROM rotation_runs ORDER BY planned_at"),
|
|
129
|
+
this.database.query("SELECT * FROM audit_events ORDER BY occurred_at"),
|
|
130
|
+
]);
|
|
131
|
+
const fullBackup = backupEnvelopeSchema.parse({
|
|
132
|
+
format: "keylore-logical-backup",
|
|
133
|
+
version: 1,
|
|
134
|
+
createdAt: new Date().toISOString(),
|
|
135
|
+
sourceVersion: this.sourceVersion,
|
|
136
|
+
tenants: tenants.rows.map((row) => ({
|
|
137
|
+
tenantId: row.tenant_id,
|
|
138
|
+
displayName: row.display_name,
|
|
139
|
+
description: row.description ?? undefined,
|
|
140
|
+
status: row.status,
|
|
141
|
+
createdAt: toIso(row.created_at),
|
|
142
|
+
updatedAt: toIso(row.updated_at),
|
|
143
|
+
})),
|
|
144
|
+
credentials: credentials.rows.map((row) => ({
|
|
145
|
+
id: row.id,
|
|
146
|
+
tenantId: row.tenant_id,
|
|
147
|
+
displayName: row.display_name,
|
|
148
|
+
service: row.service,
|
|
149
|
+
owner: row.owner,
|
|
150
|
+
scopeTier: row.scope_tier,
|
|
151
|
+
sensitivity: row.sensitivity,
|
|
152
|
+
allowedDomains: row.allowed_domains,
|
|
153
|
+
permittedOperations: row.permitted_operations,
|
|
154
|
+
expiresAt: toIso(row.expires_at),
|
|
155
|
+
rotationPolicy: row.rotation_policy,
|
|
156
|
+
lastValidatedAt: toIso(row.last_validated_at),
|
|
157
|
+
selectionNotes: row.selection_notes,
|
|
158
|
+
binding: row.binding,
|
|
159
|
+
tags: row.tags,
|
|
160
|
+
status: row.status,
|
|
161
|
+
})),
|
|
162
|
+
policies: {
|
|
163
|
+
version: 1,
|
|
164
|
+
rules: policies.rows.map((row) => ({
|
|
165
|
+
id: row.id,
|
|
166
|
+
tenantId: row.tenant_id,
|
|
167
|
+
effect: row.effect,
|
|
168
|
+
description: row.description,
|
|
169
|
+
principals: row.principals,
|
|
170
|
+
principalRoles: row.principal_roles ?? undefined,
|
|
171
|
+
credentialIds: row.credential_ids ?? undefined,
|
|
172
|
+
services: row.services ?? undefined,
|
|
173
|
+
operations: row.operations,
|
|
174
|
+
domainPatterns: row.domain_patterns,
|
|
175
|
+
environments: row.environments ?? undefined,
|
|
176
|
+
})),
|
|
177
|
+
},
|
|
178
|
+
authClients: authClients.rows.map((row) => storedAuthClientSchema.parse({
|
|
179
|
+
clientId: row.client_id,
|
|
180
|
+
tenantId: row.tenant_id,
|
|
181
|
+
displayName: row.display_name,
|
|
182
|
+
roles: row.roles,
|
|
183
|
+
allowedScopes: row.allowed_scopes,
|
|
184
|
+
status: row.status,
|
|
185
|
+
tokenEndpointAuthMethod: row.token_endpoint_auth_method,
|
|
186
|
+
grantTypes: row.grant_types,
|
|
187
|
+
redirectUris: row.redirect_uris,
|
|
188
|
+
jwks: normalizeJwks(row.jwks),
|
|
189
|
+
secretHash: row.secret_hash ?? undefined,
|
|
190
|
+
secretSalt: row.secret_salt ?? undefined,
|
|
191
|
+
})),
|
|
192
|
+
accessTokens: accessTokens.rows.map((row) => ({
|
|
193
|
+
tokenId: row.token_id,
|
|
194
|
+
tokenHash: row.token_hash,
|
|
195
|
+
clientId: row.client_id,
|
|
196
|
+
tenantId: row.tenant_id,
|
|
197
|
+
subject: row.subject,
|
|
198
|
+
scopes: row.scopes,
|
|
199
|
+
roles: row.roles,
|
|
200
|
+
resource: row.resource ?? undefined,
|
|
201
|
+
expiresAt: toIso(row.expires_at),
|
|
202
|
+
status: row.status,
|
|
203
|
+
createdAt: toIso(row.created_at),
|
|
204
|
+
lastUsedAt: toIso(row.last_used_at) ?? undefined,
|
|
205
|
+
})),
|
|
206
|
+
refreshTokens: refreshTokens.rows.map((row) => ({
|
|
207
|
+
refreshTokenId: row.refresh_token_id,
|
|
208
|
+
tokenHash: row.token_hash,
|
|
209
|
+
clientId: row.client_id,
|
|
210
|
+
tenantId: row.tenant_id,
|
|
211
|
+
subject: row.subject,
|
|
212
|
+
scopes: row.scopes,
|
|
213
|
+
roles: row.roles,
|
|
214
|
+
resource: row.resource ?? undefined,
|
|
215
|
+
expiresAt: toIso(row.expires_at),
|
|
216
|
+
status: row.status,
|
|
217
|
+
createdAt: toIso(row.created_at),
|
|
218
|
+
lastUsedAt: toIso(row.last_used_at) ?? undefined,
|
|
219
|
+
})),
|
|
220
|
+
approvals: approvals.rows.map((row) => ({
|
|
221
|
+
id: row.id,
|
|
222
|
+
tenantId: row.tenant_id,
|
|
223
|
+
createdAt: toIso(row.created_at),
|
|
224
|
+
expiresAt: toIso(row.expires_at),
|
|
225
|
+
status: row.status,
|
|
226
|
+
requestedBy: row.requested_by,
|
|
227
|
+
requestedRoles: row.requested_roles,
|
|
228
|
+
credentialId: row.credential_id,
|
|
229
|
+
operation: row.operation,
|
|
230
|
+
targetUrl: row.target_url,
|
|
231
|
+
targetHost: row.target_host,
|
|
232
|
+
reason: row.reason,
|
|
233
|
+
ruleId: row.rule_id ?? undefined,
|
|
234
|
+
correlationId: row.correlation_id,
|
|
235
|
+
fingerprint: row.fingerprint,
|
|
236
|
+
reviewedBy: row.reviewed_by ?? undefined,
|
|
237
|
+
reviewedAt: toIso(row.reviewed_at) ?? undefined,
|
|
238
|
+
reviewNote: row.review_note ?? undefined,
|
|
239
|
+
requiredApprovals: row.required_approvals,
|
|
240
|
+
approvalCount: row.approval_count,
|
|
241
|
+
denialCount: row.denial_count,
|
|
242
|
+
reviews: row.reviews,
|
|
243
|
+
})),
|
|
244
|
+
breakGlassRequests: breakGlassRequests.rows.map((row) => ({
|
|
245
|
+
id: row.id,
|
|
246
|
+
tenantId: row.tenant_id,
|
|
247
|
+
createdAt: toIso(row.created_at),
|
|
248
|
+
expiresAt: toIso(row.expires_at),
|
|
249
|
+
status: row.status,
|
|
250
|
+
requestedBy: row.requested_by,
|
|
251
|
+
requestedRoles: row.requested_roles,
|
|
252
|
+
credentialId: row.credential_id,
|
|
253
|
+
operation: row.operation,
|
|
254
|
+
targetUrl: row.target_url,
|
|
255
|
+
targetHost: row.target_host,
|
|
256
|
+
justification: row.justification,
|
|
257
|
+
requestedDurationSeconds: row.requested_duration_seconds,
|
|
258
|
+
correlationId: row.correlation_id,
|
|
259
|
+
fingerprint: row.fingerprint,
|
|
260
|
+
reviewedBy: row.reviewed_by ?? undefined,
|
|
261
|
+
reviewedAt: toIso(row.reviewed_at) ?? undefined,
|
|
262
|
+
reviewNote: row.review_note ?? undefined,
|
|
263
|
+
requiredApprovals: row.required_approvals,
|
|
264
|
+
approvalCount: row.approval_count,
|
|
265
|
+
denialCount: row.denial_count,
|
|
266
|
+
reviews: row.reviews,
|
|
267
|
+
revokedBy: row.revoked_by ?? undefined,
|
|
268
|
+
revokedAt: toIso(row.revoked_at) ?? undefined,
|
|
269
|
+
revokeNote: row.revoke_note ?? undefined,
|
|
270
|
+
})),
|
|
271
|
+
rotationRuns: rotationRuns.rows.map((row) => ({
|
|
272
|
+
id: row.id,
|
|
273
|
+
tenantId: row.tenant_id,
|
|
274
|
+
credentialId: row.credential_id,
|
|
275
|
+
status: row.status,
|
|
276
|
+
source: row.source,
|
|
277
|
+
reason: row.reason,
|
|
278
|
+
dueAt: toIso(row.due_at) ?? undefined,
|
|
279
|
+
plannedAt: toIso(row.planned_at),
|
|
280
|
+
startedAt: toIso(row.started_at) ?? undefined,
|
|
281
|
+
completedAt: toIso(row.completed_at) ?? undefined,
|
|
282
|
+
plannedBy: row.planned_by,
|
|
283
|
+
updatedBy: row.updated_by,
|
|
284
|
+
note: row.note ?? undefined,
|
|
285
|
+
targetRef: row.target_ref ?? undefined,
|
|
286
|
+
resultNote: row.result_note ?? undefined,
|
|
287
|
+
})),
|
|
288
|
+
auditEvents: auditEvents.rows.map((row) => ({
|
|
289
|
+
eventId: row.event_id,
|
|
290
|
+
occurredAt: toIso(row.occurred_at),
|
|
291
|
+
tenantId: row.tenant_id,
|
|
292
|
+
type: row.type,
|
|
293
|
+
action: row.action,
|
|
294
|
+
outcome: row.outcome,
|
|
295
|
+
principal: row.principal,
|
|
296
|
+
correlationId: row.correlation_id,
|
|
297
|
+
metadata: row.metadata,
|
|
298
|
+
})),
|
|
299
|
+
});
|
|
300
|
+
const backup = this.filterBackupForTenant(fullBackup, actor?.tenantId);
|
|
301
|
+
if (actor) {
|
|
302
|
+
await this.audit.record({
|
|
303
|
+
type: "system.backup",
|
|
304
|
+
action: "system.backup.export",
|
|
305
|
+
outcome: "success",
|
|
306
|
+
tenantId: actor.tenantId,
|
|
307
|
+
principal: actor.principal,
|
|
308
|
+
metadata: this.summarizeBackup(backup),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return backup;
|
|
312
|
+
}
|
|
313
|
+
async writeBackup(filePath, actor) {
|
|
314
|
+
const backup = await this.exportBackup(actor);
|
|
315
|
+
await fs.writeFile(filePath, `${JSON.stringify(backup, null, 2)}\n`, "utf8");
|
|
316
|
+
return backup;
|
|
317
|
+
}
|
|
318
|
+
async readBackup(filePath) {
|
|
319
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
320
|
+
return this.parseBackupPayload(JSON.parse(raw));
|
|
321
|
+
}
|
|
322
|
+
async restoreBackupPayload(backup, actor) {
|
|
323
|
+
if (actor?.tenantId) {
|
|
324
|
+
this.assertTenantScopedBackup(backup, actor.tenantId);
|
|
325
|
+
}
|
|
326
|
+
await this.database.withTransaction(async (client) => {
|
|
327
|
+
if (actor?.tenantId) {
|
|
328
|
+
await client.query("DELETE FROM access_tokens WHERE tenant_id = $1", [actor.tenantId]);
|
|
329
|
+
await client.query("DELETE FROM refresh_tokens WHERE tenant_id = $1", [actor.tenantId]);
|
|
330
|
+
await client.query("DELETE FROM approval_requests WHERE tenant_id = $1", [actor.tenantId]);
|
|
331
|
+
await client.query("DELETE FROM break_glass_requests WHERE tenant_id = $1", [actor.tenantId]);
|
|
332
|
+
await client.query("DELETE FROM rotation_runs WHERE tenant_id = $1", [actor.tenantId]);
|
|
333
|
+
await client.query("DELETE FROM audit_events WHERE tenant_id = $1", [actor.tenantId]);
|
|
334
|
+
await client.query("DELETE FROM oauth_clients WHERE tenant_id = $1", [actor.tenantId]);
|
|
335
|
+
await client.query("DELETE FROM policy_rules WHERE tenant_id = $1", [actor.tenantId]);
|
|
336
|
+
await client.query("DELETE FROM credentials WHERE tenant_id = $1", [actor.tenantId]);
|
|
337
|
+
await client.query("DELETE FROM tenants WHERE tenant_id = $1", [actor.tenantId]);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
await client.query("DELETE FROM access_tokens");
|
|
341
|
+
await client.query("DELETE FROM refresh_tokens");
|
|
342
|
+
await client.query("DELETE FROM approval_requests");
|
|
343
|
+
await client.query("DELETE FROM break_glass_requests");
|
|
344
|
+
await client.query("DELETE FROM rotation_runs");
|
|
345
|
+
await client.query("DELETE FROM audit_events");
|
|
346
|
+
await client.query("DELETE FROM oauth_clients");
|
|
347
|
+
await client.query("DELETE FROM policy_rules");
|
|
348
|
+
await client.query("DELETE FROM credentials");
|
|
349
|
+
await client.query("DELETE FROM tenants");
|
|
350
|
+
await client.query("DELETE FROM request_rate_limits");
|
|
351
|
+
}
|
|
352
|
+
for (const tenant of backup.tenants) {
|
|
353
|
+
await client.query(`INSERT INTO tenants (
|
|
354
|
+
tenant_id, display_name, description, status, created_at, updated_at
|
|
355
|
+
) VALUES ($1, $2, $3, $4, $5, $6)`, [
|
|
356
|
+
tenant.tenantId,
|
|
357
|
+
tenant.displayName,
|
|
358
|
+
tenant.description ?? null,
|
|
359
|
+
tenant.status,
|
|
360
|
+
tenant.createdAt,
|
|
361
|
+
tenant.updatedAt,
|
|
362
|
+
]);
|
|
363
|
+
}
|
|
364
|
+
for (const credential of backup.credentials) {
|
|
365
|
+
await client.query(`INSERT INTO credentials (
|
|
366
|
+
id, tenant_id, display_name, service, owner, scope_tier, sensitivity,
|
|
367
|
+
allowed_domains, permitted_operations, expires_at, rotation_policy,
|
|
368
|
+
last_validated_at, selection_notes, binding, tags, status
|
|
369
|
+
) VALUES (
|
|
370
|
+
$1, $2, $3, $4, $5, $6, $7,
|
|
371
|
+
$8, $9, $10, $11,
|
|
372
|
+
$12, $13, $14, $15, $16
|
|
373
|
+
)`, [
|
|
374
|
+
credential.id,
|
|
375
|
+
credential.tenantId,
|
|
376
|
+
credential.displayName,
|
|
377
|
+
credential.service,
|
|
378
|
+
credential.owner,
|
|
379
|
+
credential.scopeTier,
|
|
380
|
+
credential.sensitivity,
|
|
381
|
+
credential.allowedDomains,
|
|
382
|
+
credential.permittedOperations,
|
|
383
|
+
credential.expiresAt,
|
|
384
|
+
credential.rotationPolicy,
|
|
385
|
+
credential.lastValidatedAt,
|
|
386
|
+
credential.selectionNotes,
|
|
387
|
+
credential.binding,
|
|
388
|
+
credential.tags,
|
|
389
|
+
credential.status,
|
|
390
|
+
]);
|
|
391
|
+
}
|
|
392
|
+
for (const rule of backup.policies.rules) {
|
|
393
|
+
await client.query(`INSERT INTO policy_rules (
|
|
394
|
+
id, tenant_id, effect, description, principals, principal_roles, credential_ids,
|
|
395
|
+
services, operations, domain_patterns, environments
|
|
396
|
+
) VALUES (
|
|
397
|
+
$1, $2, $3, $4, $5, $6, $7,
|
|
398
|
+
$8, $9, $10, $11
|
|
399
|
+
)`, [
|
|
400
|
+
rule.id,
|
|
401
|
+
rule.tenantId,
|
|
402
|
+
rule.effect,
|
|
403
|
+
rule.description,
|
|
404
|
+
rule.principals,
|
|
405
|
+
rule.principalRoles ?? null,
|
|
406
|
+
rule.credentialIds ?? null,
|
|
407
|
+
rule.services ?? null,
|
|
408
|
+
rule.operations,
|
|
409
|
+
rule.domainPatterns,
|
|
410
|
+
rule.environments ?? null,
|
|
411
|
+
]);
|
|
412
|
+
}
|
|
413
|
+
for (const clientRecord of backup.authClients) {
|
|
414
|
+
await client.query(`INSERT INTO oauth_clients (
|
|
415
|
+
client_id, tenant_id, display_name, secret_hash, secret_salt, roles, allowed_scopes, status,
|
|
416
|
+
token_endpoint_auth_method, grant_types, redirect_uris, jwks
|
|
417
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, [
|
|
418
|
+
clientRecord.clientId,
|
|
419
|
+
clientRecord.tenantId,
|
|
420
|
+
clientRecord.displayName,
|
|
421
|
+
clientRecord.secretHash ?? null,
|
|
422
|
+
clientRecord.secretSalt ?? null,
|
|
423
|
+
clientRecord.roles,
|
|
424
|
+
clientRecord.allowedScopes,
|
|
425
|
+
clientRecord.status,
|
|
426
|
+
clientRecord.tokenEndpointAuthMethod,
|
|
427
|
+
clientRecord.grantTypes,
|
|
428
|
+
clientRecord.redirectUris,
|
|
429
|
+
clientRecord.jwks,
|
|
430
|
+
]);
|
|
431
|
+
}
|
|
432
|
+
for (const token of backup.accessTokens) {
|
|
433
|
+
await client.query(`INSERT INTO access_tokens (
|
|
434
|
+
token_id, token_hash, client_id, tenant_id, subject, scopes, roles,
|
|
435
|
+
resource, expires_at, status, created_at, last_used_at
|
|
436
|
+
) VALUES (
|
|
437
|
+
$1, $2, $3, $4, $5, $6, $7,
|
|
438
|
+
$8, $9, $10, $11, $12
|
|
439
|
+
)`, [
|
|
440
|
+
token.tokenId,
|
|
441
|
+
token.tokenHash,
|
|
442
|
+
token.clientId,
|
|
443
|
+
token.tenantId,
|
|
444
|
+
token.subject,
|
|
445
|
+
token.scopes,
|
|
446
|
+
token.roles,
|
|
447
|
+
token.resource ?? null,
|
|
448
|
+
token.expiresAt,
|
|
449
|
+
token.status,
|
|
450
|
+
token.createdAt,
|
|
451
|
+
token.lastUsedAt ?? null,
|
|
452
|
+
]);
|
|
453
|
+
}
|
|
454
|
+
for (const token of backup.refreshTokens) {
|
|
455
|
+
await client.query(`INSERT INTO refresh_tokens (
|
|
456
|
+
refresh_token_id, token_hash, client_id, tenant_id, subject, scopes, roles,
|
|
457
|
+
resource, expires_at, status, created_at, last_used_at
|
|
458
|
+
) VALUES (
|
|
459
|
+
$1, $2, $3, $4, $5, $6, $7,
|
|
460
|
+
$8, $9, $10, $11, $12
|
|
461
|
+
)`, [
|
|
462
|
+
token.refreshTokenId,
|
|
463
|
+
token.tokenHash,
|
|
464
|
+
token.clientId,
|
|
465
|
+
token.tenantId,
|
|
466
|
+
token.subject,
|
|
467
|
+
token.scopes,
|
|
468
|
+
token.roles,
|
|
469
|
+
token.resource ?? null,
|
|
470
|
+
token.expiresAt,
|
|
471
|
+
token.status,
|
|
472
|
+
token.createdAt,
|
|
473
|
+
token.lastUsedAt ?? null,
|
|
474
|
+
]);
|
|
475
|
+
}
|
|
476
|
+
for (const approval of backup.approvals) {
|
|
477
|
+
await client.query(`INSERT INTO approval_requests (
|
|
478
|
+
id, tenant_id, created_at, expires_at, status, requested_by, requested_roles,
|
|
479
|
+
credential_id, operation, target_url, target_host, reason, rule_id,
|
|
480
|
+
correlation_id, fingerprint, reviewed_by, reviewed_at, review_note,
|
|
481
|
+
required_approvals, approval_count, denial_count, reviews
|
|
482
|
+
) VALUES (
|
|
483
|
+
$1, $2, $3, $4, $5, $6, $7,
|
|
484
|
+
$8, $9, $10, $11, $12, $13,
|
|
485
|
+
$14, $15, $16, $17, $18,
|
|
486
|
+
$19, $20, $21, $22
|
|
487
|
+
)`, [
|
|
488
|
+
approval.id,
|
|
489
|
+
approval.tenantId,
|
|
490
|
+
approval.createdAt,
|
|
491
|
+
approval.expiresAt,
|
|
492
|
+
approval.status,
|
|
493
|
+
approval.requestedBy,
|
|
494
|
+
approval.requestedRoles,
|
|
495
|
+
approval.credentialId,
|
|
496
|
+
approval.operation,
|
|
497
|
+
approval.targetUrl,
|
|
498
|
+
approval.targetHost,
|
|
499
|
+
approval.reason,
|
|
500
|
+
approval.ruleId ?? null,
|
|
501
|
+
approval.correlationId,
|
|
502
|
+
approval.fingerprint,
|
|
503
|
+
approval.reviewedBy ?? null,
|
|
504
|
+
approval.reviewedAt ?? null,
|
|
505
|
+
approval.reviewNote ?? null,
|
|
506
|
+
approval.requiredApprovals,
|
|
507
|
+
approval.approvalCount,
|
|
508
|
+
approval.denialCount,
|
|
509
|
+
approval.reviews,
|
|
510
|
+
]);
|
|
511
|
+
}
|
|
512
|
+
for (const request of backup.breakGlassRequests) {
|
|
513
|
+
await client.query(`INSERT INTO break_glass_requests (
|
|
514
|
+
id, tenant_id, created_at, expires_at, status, requested_by, requested_roles,
|
|
515
|
+
credential_id, operation, target_url, target_host, justification, requested_duration_seconds,
|
|
516
|
+
correlation_id, fingerprint, reviewed_by, reviewed_at, review_note,
|
|
517
|
+
required_approvals, approval_count, denial_count, reviews,
|
|
518
|
+
revoked_by, revoked_at, revoke_note
|
|
519
|
+
) VALUES (
|
|
520
|
+
$1, $2, $3, $4, $5, $6, $7,
|
|
521
|
+
$8, $9, $10, $11, $12, $13,
|
|
522
|
+
$14, $15, $16, $17, $18,
|
|
523
|
+
$19, $20, $21, $22,
|
|
524
|
+
$23, $24, $25
|
|
525
|
+
)`, [
|
|
526
|
+
request.id,
|
|
527
|
+
request.tenantId,
|
|
528
|
+
request.createdAt,
|
|
529
|
+
request.expiresAt,
|
|
530
|
+
request.status,
|
|
531
|
+
request.requestedBy,
|
|
532
|
+
request.requestedRoles,
|
|
533
|
+
request.credentialId,
|
|
534
|
+
request.operation,
|
|
535
|
+
request.targetUrl,
|
|
536
|
+
request.targetHost,
|
|
537
|
+
request.justification,
|
|
538
|
+
request.requestedDurationSeconds,
|
|
539
|
+
request.correlationId,
|
|
540
|
+
request.fingerprint,
|
|
541
|
+
request.reviewedBy ?? null,
|
|
542
|
+
request.reviewedAt ?? null,
|
|
543
|
+
request.reviewNote ?? null,
|
|
544
|
+
request.requiredApprovals,
|
|
545
|
+
request.approvalCount,
|
|
546
|
+
request.denialCount,
|
|
547
|
+
request.reviews,
|
|
548
|
+
request.revokedBy ?? null,
|
|
549
|
+
request.revokedAt ?? null,
|
|
550
|
+
request.revokeNote ?? null,
|
|
551
|
+
]);
|
|
552
|
+
}
|
|
553
|
+
for (const rotation of backup.rotationRuns) {
|
|
554
|
+
await client.query(`INSERT INTO rotation_runs (
|
|
555
|
+
id, tenant_id, credential_id, status, source, reason, due_at, planned_at, started_at,
|
|
556
|
+
completed_at, planned_by, updated_by, note, target_ref, result_note
|
|
557
|
+
) VALUES (
|
|
558
|
+
$1, $2, $3, $4, $5, $6, $7, $8, $9,
|
|
559
|
+
$10, $11, $12, $13, $14, $15
|
|
560
|
+
)`, [
|
|
561
|
+
rotation.id,
|
|
562
|
+
rotation.tenantId,
|
|
563
|
+
rotation.credentialId,
|
|
564
|
+
rotation.status,
|
|
565
|
+
rotation.source,
|
|
566
|
+
rotation.reason,
|
|
567
|
+
rotation.dueAt ?? null,
|
|
568
|
+
rotation.plannedAt,
|
|
569
|
+
rotation.startedAt ?? null,
|
|
570
|
+
rotation.completedAt ?? null,
|
|
571
|
+
rotation.plannedBy,
|
|
572
|
+
rotation.updatedBy,
|
|
573
|
+
rotation.note ?? null,
|
|
574
|
+
rotation.targetRef ?? null,
|
|
575
|
+
rotation.resultNote ?? null,
|
|
576
|
+
]);
|
|
577
|
+
}
|
|
578
|
+
for (const event of backup.auditEvents) {
|
|
579
|
+
await client.query(`INSERT INTO audit_events (
|
|
580
|
+
event_id, occurred_at, tenant_id, type, action, outcome, principal, correlation_id, metadata
|
|
581
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [
|
|
582
|
+
event.eventId,
|
|
583
|
+
event.occurredAt,
|
|
584
|
+
event.tenantId,
|
|
585
|
+
event.type,
|
|
586
|
+
event.action,
|
|
587
|
+
event.outcome,
|
|
588
|
+
event.principal,
|
|
589
|
+
event.correlationId,
|
|
590
|
+
event.metadata,
|
|
591
|
+
]);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
if (actor) {
|
|
595
|
+
await this.audit.record({
|
|
596
|
+
type: "system.backup",
|
|
597
|
+
action: "system.backup.restore",
|
|
598
|
+
outcome: "success",
|
|
599
|
+
tenantId: actor.tenantId,
|
|
600
|
+
principal: actor.principal,
|
|
601
|
+
metadata: this.summarizeBackup(backup),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
return backup;
|
|
605
|
+
}
|
|
606
|
+
async restoreBackup(filePath, actor) {
|
|
607
|
+
const backup = await this.readBackup(filePath);
|
|
608
|
+
return this.restoreBackupPayload(backup, actor);
|
|
609
|
+
}
|
|
610
|
+
}
|