feat: secure organization provider credentials

This commit is contained in:
2026-07-10 22:04:43 +08:00
parent e049cb6880
commit 848f913597
51 changed files with 3280 additions and 230 deletions
+65
View File
@@ -41,6 +41,8 @@ model Organization {
projects Project[]
teams Team[]
externalDirectoryConnections ExternalDirectoryConnection[]
providerConnections OrganizationProviderConnection[]
auditEntries AuditEntry[] @relation("organizationAudit")
@@index([status])
}
@@ -106,6 +108,66 @@ model User {
permissionGrants PermissionGrant[] @relation("grantCreator")
roleTriggerGrants RoleTriggerGrant[] @relation("roleGrantCreator")
auditEntries AuditEntry[] @relation("auditActor")
providerCredentialVersions ProviderCredentialVersion[] @relation("providerCredentialVersionCreator")
}
/// ADR-0024: model-provider connection identity is stable and belongs to one
/// Organization. Both BYOK and platform-managed credentials are tenant-local;
/// only ACTIVE connections with an active immutable secret version resolve.
model OrganizationProviderConnection {
id String @id @default(cuid())
organizationId String
providerId String
mode ProviderCredentialMode
status OrganizationConnectionStatus @default(DRAFT)
activeSecretVersionId String? @unique
activatedAt DateTime?
disabledAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
secretVersions ProviderCredentialVersion[] @relation("providerCredentialVersions")
activeSecretVersion ProviderCredentialVersion? @relation("activeProviderCredentialVersion", fields: [activeSecretVersionId], references: [id], onDelete: Restrict)
@@unique([organizationId, providerId])
@@index([organizationId, status])
@@index([providerId, status])
}
enum ProviderCredentialMode {
BYOK
PLATFORM_MANAGED
}
enum OrganizationConnectionStatus {
DRAFT
ACTIVE
DISABLED
}
/// ADR-0024: write-only provider secret material is one authenticated envelope
/// per immutable version. keyId/envelopeVersion are redacted rotation metadata;
/// all provider URL/token/API-key fields remain inside envelope ciphertext.
model ProviderCredentialVersion {
id String @id @default(cuid())
connectionId String
version Int
envelopeVersion Int @default(1)
keyId String
envelope Json
createdByUserId String?
createdAt DateTime @default(now())
retiredAt DateTime?
connection OrganizationProviderConnection @relation("providerCredentialVersions", fields: [connectionId], references: [id], onDelete: Cascade)
activeFor OrganizationProviderConnection? @relation("activeProviderCredentialVersion")
createdBy User? @relation("providerCredentialVersionCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
@@unique([connectionId, version])
@@index([connectionId, retiredAt])
@@index([keyId])
@@index([createdByUserId])
}
/// Platform-level role (admin/teacher). Distinct from ADR-0004 PermissionRole.
@@ -501,6 +563,7 @@ model AuditEntry {
id String @id @default(cuid())
runId String?
projectId String?
organizationId String?
actorUserId String?
action String
metadata Json
@@ -508,9 +571,11 @@ model AuditEntry {
actor User? @relation("auditActor", fields: [actorUserId], references: [id], onDelete: SetNull)
project Project? @relation("projectAudit", fields: [projectId], references: [id], onDelete: SetNull)
organization Organization? @relation("organizationAudit", fields: [organizationId], references: [id], onDelete: SetNull)
@@index([runId])
@@index([projectId, createdAt])
@@index([organizationId, createdAt])
@@index([actorUserId])
@@index([createdAt])
}