forked from bai/curriculum-project-hub
feat: secure organization provider credentials
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
-- ADR-0024: Organization-scoped provider connections and immutable encrypted
|
||||
-- credential versions. No provider plaintext column exists in this schema.
|
||||
|
||||
CREATE TYPE "ProviderCredentialMode" AS ENUM ('BYOK', 'PLATFORM_MANAGED');
|
||||
CREATE TYPE "OrganizationConnectionStatus" AS ENUM ('DRAFT', 'ACTIVE', 'DISABLED');
|
||||
|
||||
CREATE TABLE "OrganizationProviderConnection" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"mode" "ProviderCredentialMode" NOT NULL,
|
||||
"status" "OrganizationConnectionStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"activeSecretVersionId" TEXT,
|
||||
"activatedAt" TIMESTAMP(3),
|
||||
"disabledAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "OrganizationProviderConnection_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "ProviderCredentialVersion" (
|
||||
"id" TEXT NOT NULL,
|
||||
"connectionId" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"envelopeVersion" INTEGER NOT NULL DEFAULT 1,
|
||||
"keyId" TEXT NOT NULL,
|
||||
"envelope" JSONB NOT NULL,
|
||||
"createdByUserId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"retiredAt" TIMESTAMP(3),
|
||||
CONSTRAINT "ProviderCredentialVersion_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "OrganizationProviderConnection_organizationId_providerId_key"
|
||||
ON "OrganizationProviderConnection"("organizationId", "providerId");
|
||||
CREATE UNIQUE INDEX "OrganizationProviderConnection_activeSecretVersionId_key"
|
||||
ON "OrganizationProviderConnection"("activeSecretVersionId");
|
||||
CREATE INDEX "OrganizationProviderConnection_organizationId_status_idx"
|
||||
ON "OrganizationProviderConnection"("organizationId", "status");
|
||||
CREATE INDEX "OrganizationProviderConnection_providerId_status_idx"
|
||||
ON "OrganizationProviderConnection"("providerId", "status");
|
||||
|
||||
CREATE UNIQUE INDEX "ProviderCredentialVersion_connectionId_version_key"
|
||||
ON "ProviderCredentialVersion"("connectionId", "version");
|
||||
CREATE INDEX "ProviderCredentialVersion_connectionId_retiredAt_idx"
|
||||
ON "ProviderCredentialVersion"("connectionId", "retiredAt");
|
||||
CREATE INDEX "ProviderCredentialVersion_keyId_idx"
|
||||
ON "ProviderCredentialVersion"("keyId");
|
||||
CREATE INDEX "ProviderCredentialVersion_createdByUserId_idx"
|
||||
ON "ProviderCredentialVersion"("createdByUserId");
|
||||
|
||||
ALTER TABLE "OrganizationProviderConnection"
|
||||
ADD CONSTRAINT "OrganizationProviderConnection_organizationId_fkey"
|
||||
FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "ProviderCredentialVersion"
|
||||
ADD CONSTRAINT "ProviderCredentialVersion_connectionId_fkey"
|
||||
FOREIGN KEY ("connectionId") REFERENCES "OrganizationProviderConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "ProviderCredentialVersion"
|
||||
ADD CONSTRAINT "ProviderCredentialVersion_createdByUserId_fkey"
|
||||
FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "OrganizationProviderConnection"
|
||||
ADD CONSTRAINT "OrganizationProviderConnection_activeSecretVersionId_fkey"
|
||||
FOREIGN KEY ("activeSecretVersionId") REFERENCES "ProviderCredentialVersion"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "AuditEntry" ADD COLUMN "organizationId" TEXT;
|
||||
ALTER TABLE "AuditEntry"
|
||||
ADD CONSTRAINT "AuditEntry_organizationId_fkey"
|
||||
FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
CREATE INDEX "AuditEntry_organizationId_createdAt_idx"
|
||||
ON "AuditEntry"("organizationId", "createdAt");
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user