Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion apps/webapp/app/services/directorySyncEffects.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,19 @@ async function applyEffect(effect: DirectorySyncEffect): Promise<void> {
roleId: effect.roleId,
});
if (!result.ok) {
throw retryableEffectError(`directorySync provision setUserRole failed: ${result.error}`);
// The org must keep one Owner: skip the role overwrite for the last
// Owner (they keep Owner) instead of failing the whole batch. Applies
// to a directory burst and to a dashboard group remap alike.
if (result.code === "last_owner") {
logger.info("directorySync: kept last Owner, skipped provision role overwrite", {
userId,
organizationId: effect.organizationId,
});
} else {
throw retryableEffectError(
`directorySync provision setUserRole failed: ${result.error}`
);
}
Comment thread
0ski marked this conversation as resolved.
}
}
return;
Expand All @@ -107,6 +119,15 @@ async function applyEffect(effect: DirectorySyncEffect): Promise<void> {
roleId: effect.roleId,
});
if (!result.ok) {
// Keeping the org's last Owner is expected, not a failure — skip this
// one member and let the rest of the remap apply (no server error).
if (result.code === "last_owner") {
logger.info("directorySync: kept last Owner, skipped set_role", {
userId: effect.userId,
organizationId: effect.organizationId,
});
return;
}
throw retryableEffectError(`directorySync set_role failed: ${result.error}`);
}
return;
Expand Down
8 changes: 7 additions & 1 deletion packages/plugins/src/rbac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,13 @@ export interface RoleBaseAccessController {
export type RoleMutationResult = { ok: true; role: Role } | { ok: false; error: string };

// Result for assignment / deletion mutations that don't return a value.
export type RoleAssignmentResult = { ok: true } | { ok: false; error: string };
// `code` is an optional machine-readable reason so callers can branch on
// expected outcomes (e.g. `last_owner`, the guard that keeps an org from
// losing its final Owner) instead of matching the free-text `error`.
export type RoleAssignmentErrorCode = "last_owner";
export type RoleAssignmentResult =
| { ok: true }
| { ok: false; error: string; code?: RoleAssignmentErrorCode };

import type { PluginDatabaseConfig } from "./databaseConfig.js";

Expand Down
Loading