240 lines
6.6 KiB
TypeScript
240 lines
6.6 KiB
TypeScript
import type {
|
|
DirectorySyncEffect,
|
|
DirectorySyncStatus,
|
|
OrgSsoStatus,
|
|
SsoBeginError,
|
|
SsoCompleteError,
|
|
SsoController,
|
|
SsoDecisionError,
|
|
SsoFlow,
|
|
SsoMutationError,
|
|
SsoPortalError,
|
|
SsoProfile,
|
|
SsoResolutionDecision,
|
|
SsoRouteDecision,
|
|
SsoValidateError,
|
|
SsoWebhookError,
|
|
SsoWebhookEvent,
|
|
} from "@trigger.dev/plugins";
|
|
import { errAsync, okAsync, type ResultAsync } from "neverthrow";
|
|
|
|
// The default fallback used when no cloud SSO plugin is installed.
|
|
// `decideRouteForEmail` returns no_sso so OSS deployments behave
|
|
// identically to a deployment with no SSO feature at all. Mutation
|
|
// methods return feature_disabled so callers can surface a clear
|
|
// "not available" message in UI gated by `isUsingPlugin()`.
|
|
//
|
|
// The fallback never touches the database. It still accepts the loader's
|
|
// Prisma input for signature parity with the real cloud plugin factory
|
|
// (so the loader can swap implementations without changing its call),
|
|
// but ignores it entirely.
|
|
export class SsoFallback {
|
|
constructor(_prisma?: unknown) {}
|
|
|
|
create(): SsoController {
|
|
return new SsoFallbackController();
|
|
}
|
|
}
|
|
|
|
class SsoFallbackController implements SsoController {
|
|
async isUsingPlugin(): Promise<boolean> {
|
|
return false;
|
|
}
|
|
|
|
getStatus(_organizationId: string): ResultAsync<OrgSsoStatus, SsoDecisionError> {
|
|
return okAsync({
|
|
hasIdpOrg: false,
|
|
enforced: false,
|
|
jitProvisioningEnabled: false,
|
|
jitDefaultRoleId: null,
|
|
idpOrgId: null,
|
|
primaryConnectionId: null,
|
|
domains: [],
|
|
connections: [],
|
|
});
|
|
}
|
|
|
|
generatePortalLink(_params: {
|
|
organizationId: string;
|
|
userId: string;
|
|
intent: "sso" | "domain_verification" | "dsync";
|
|
returnUrl: string;
|
|
}): ResultAsync<{ url: string }, SsoPortalError> {
|
|
return errAsync("idp_org_unavailable" as const);
|
|
}
|
|
|
|
setEnforced(_params: {
|
|
organizationId: string;
|
|
enforced: boolean;
|
|
}): ResultAsync<void, SsoMutationError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
setJitProvisioningEnabled(_params: {
|
|
organizationId: string;
|
|
enabled: boolean;
|
|
}): ResultAsync<void, SsoMutationError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
setJitDefaultRole(_params: {
|
|
organizationId: string;
|
|
roleId: string | null;
|
|
}): ResultAsync<void, SsoMutationError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
updateConfig(_params: {
|
|
organizationId: string;
|
|
enforced: boolean;
|
|
jitProvisioningEnabled: boolean;
|
|
jitDefaultRoleId: string | null;
|
|
}): ResultAsync<void, SsoMutationError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
getDirectorySyncStatus(
|
|
_organizationId: string
|
|
): ResultAsync<DirectorySyncStatus, SsoDecisionError> {
|
|
return okAsync({
|
|
hasDirectory: false,
|
|
hasActiveDirectory: false,
|
|
allowExternalDomainSync: false,
|
|
allowManualMembership: true,
|
|
directoryDefaultRoleId: null,
|
|
userCount: 0,
|
|
directories: [],
|
|
groups: [],
|
|
});
|
|
}
|
|
|
|
setDirectoryGroupRole(_params: {
|
|
organizationId: string;
|
|
groupId: string;
|
|
roleId: string | null;
|
|
}): ResultAsync<{ effects: DirectorySyncEffect[] }, SsoMutationError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
setDirectoryDefaultRole(_params: {
|
|
organizationId: string;
|
|
roleId: string | null;
|
|
}): ResultAsync<void, SsoMutationError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
setAllowExternalDomainSync(_params: {
|
|
organizationId: string;
|
|
allowed: boolean;
|
|
}): ResultAsync<void, SsoMutationError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
// OSS has no directory, so manual membership is always allowed.
|
|
getMembershipPolicy(
|
|
_organizationId: string
|
|
): ResultAsync<{ manualMembershipAllowed: boolean }, SsoDecisionError> {
|
|
return okAsync({ manualMembershipAllowed: true });
|
|
}
|
|
|
|
setAllowManualMembership(_params: {
|
|
organizationId: string;
|
|
allowed: boolean;
|
|
}): ResultAsync<void, SsoMutationError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
recordMembershipRemoval(_params: {
|
|
organizationId: string;
|
|
userId: string;
|
|
reason: "manual_removal" | "self_leave";
|
|
}): ResultAsync<void, SsoMutationError> {
|
|
// No plugin → no JIT → nothing to guard against. No-op success.
|
|
return okAsync(undefined as void);
|
|
}
|
|
|
|
clearMembershipRemoval(_params: {
|
|
organizationId: string;
|
|
userId: string;
|
|
}): ResultAsync<void, SsoMutationError> {
|
|
return okAsync(undefined as void);
|
|
}
|
|
|
|
decideRouteForEmail(_email: string): ResultAsync<SsoRouteDecision, SsoDecisionError> {
|
|
return okAsync({ kind: "no_sso" as const });
|
|
}
|
|
|
|
beginAuthorization(_params: {
|
|
email: string;
|
|
redirectTo: string;
|
|
flow: SsoFlow;
|
|
}): ResultAsync<{ url: string }, SsoBeginError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
completeAuthorization(_params: { code: string; state: string }): ResultAsync<
|
|
{
|
|
profile: SsoProfile;
|
|
redirectTo: string;
|
|
flow: SsoFlow;
|
|
},
|
|
SsoCompleteError
|
|
> {
|
|
return errAsync("connection_unknown" as const);
|
|
}
|
|
|
|
completeIdpInitiatedAuthorization(_params: {
|
|
code: string;
|
|
}): ResultAsync<{ profile: SsoProfile; redirectTo: string }, SsoCompleteError> {
|
|
return errAsync("connection_unknown" as const);
|
|
}
|
|
|
|
// Fail-open: with no plugin there are no SSO sessions to invalidate,
|
|
// and the host treats `valid: true` as "leave the session alone".
|
|
validateSession(_params: {
|
|
userId: string;
|
|
idpOrgId: string;
|
|
connectionId: string;
|
|
}): ResultAsync<{ valid: boolean }, SsoValidateError> {
|
|
return okAsync({ valid: true });
|
|
}
|
|
|
|
resolveSsoIdentity(_params: {
|
|
profile: SsoProfile;
|
|
}): ResultAsync<SsoResolutionDecision, SsoMutationError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
attachSsoIdentity(_params: {
|
|
userId: string;
|
|
profile: SsoProfile;
|
|
}): ResultAsync<void, SsoMutationError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
evaluateJit(_params: {
|
|
userId: string;
|
|
idpOrgId: string;
|
|
}): ResultAsync<
|
|
{ shouldProvision: boolean; organizationId: string; roleId: string | null },
|
|
SsoMutationError
|
|
> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
verifyWebhook(_params: {
|
|
rawBody: string;
|
|
headers: Record<string, string>;
|
|
}): ResultAsync<{ event: SsoWebhookEvent }, SsoWebhookError> {
|
|
return errAsync("feature_disabled" as const);
|
|
}
|
|
|
|
processWebhookEvent(
|
|
_event: SsoWebhookEvent
|
|
): ResultAsync<{ effects: DirectorySyncEffect[] }, SsoWebhookError> {
|
|
// No plugin: nothing to verify or act on. The host's webhook proxy
|
|
// already rejects unverified requests, so a call here just no-ops.
|
|
return okAsync({ effects: [] });
|
|
}
|
|
}
|