fix: require reauthorization after owner removal

Deactivate surviving grants and shared connections whose credentials belonged to a suspended or archived member so dependent runs fail closed with an actionable reauthorization state.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-21 02:35:32 +00:00
1 parent 5ae50e9a71
commit 63ddbd43da
2 files changed
+32 -4

No files matched your search

+14 -2
View File
@@ -275,6 +275,8 @@ describeEmbeddedPostgres("access service", () => {
transport: "mcp_remote",
authKind: "oauth",
credentialPolicy: "per_user",
status: "active",
enabled: true,
}).returning().then((rows) => rows[0]!);
const definition = await db.insert(userSecretDefinitions).values({
companyId: company.id,
@@ -329,9 +331,19 @@ describeEmbeddedPostgres("access service", () => {
expect(await db.select().from(connectionGrants).where(eq(connectionGrants.id, grant.id)))
.toEqual([expect.objectContaining({ status: "revoked", credentialSecretRefs: [] })]);
expect(await db.select().from(connectionGrants).where(eq(connectionGrants.id, organizationGrant.id)))
.toEqual([expect.objectContaining({ credentialSecretRefs: [] })]);
.toEqual([expect.objectContaining({
status: "needs_reauthorization",
isDefault: false,
credentialSecretRefs: [],
})]);
expect(await db.select().from(toolConnections).where(eq(toolConnections.id, connection.id)))
.toEqual([expect.objectContaining({ credentialSecretRefs: [] })]);
.toEqual([expect.objectContaining({
status: "draft",
enabled: false,
healthStatus: "missing_secret",
lastError: "oauth_reauthorization_required",
credentialSecretRefs: [],
})]);
expect(await db.select().from(toolAccessAuditEvents).where(eq(toolAccessAuditEvents.reasonCode, "membership_removed")))
.toHaveLength(1);
});
+18 -2
View File
@@ -87,6 +87,7 @@ export function accessService(db: Db) {
const [grantRefs, connectionRefs] = await Promise.all([
tx.select({
id: connectionGrants.id,
status: connectionGrants.status,
credentialSecretRefs: connectionGrants.credentialSecretRefs,
}).from(connectionGrants).where(eq(connectionGrants.companyId, companyId)),
tx.select({
@@ -99,7 +100,14 @@ export function accessService(db: Db) {
(ref) => !removedSecretIds.has(ref.secretId),
);
if (credentialSecretRefs.length !== grant.credentialSecretRefs.length) {
await tx.update(connectionGrants).set({ credentialSecretRefs, updatedAt: now })
await tx.update(connectionGrants).set({
credentialSecretRefs,
...(grant.status === "revoked" ? {} : {
status: "needs_reauthorization" as const,
isDefault: false,
}),
updatedAt: now,
})
.where(eq(connectionGrants.id, grant.id));
}
}
@@ -108,7 +116,15 @@ export function accessService(db: Db) {
(ref) => !removedSecretIds.has(ref.secretId),
);
if (credentialSecretRefs.length !== connection.credentialSecretRefs.length) {
await tx.update(toolConnections).set({ credentialSecretRefs, updatedAt: now })
await tx.update(toolConnections).set({
credentialSecretRefs,
status: "draft",
enabled: false,
healthStatus: "missing_secret",
healthMessage: "Personal credential owner no longer has company access. Reauthorize this connection.",
lastError: "oauth_reauthorization_required",
updatedAt: now,
})
.where(eq(toolConnections.id, connection.id));
}
}