mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: move sso and ldap logins into plugins (C2)
This commit is contained in:
1 parent
5c18435105
commit
925da4a661
223 files changed
+25778
-6820
No files matched your search
@@ -8,6 +8,7 @@
|
||||
|
||||
import type {
|
||||
PluginAuth,
|
||||
PluginVerifiedIdentity,
|
||||
PluginSsh,
|
||||
PluginSshConnectOptions,
|
||||
PluginSshHost,
|
||||
@@ -28,7 +29,12 @@ import {
|
||||
} from "../hosts/connect/auth-provider-registry.js";
|
||||
import { classifyKeyboardInteractive } from "../hosts/connect/keyboard-interactive.js";
|
||||
import { ensureCoreSshAuthProviders } from "../hosts/connect/core-providers.js";
|
||||
import { registerLoginMethod, registerSecondFactor } from "../auth/registry.js";
|
||||
import {
|
||||
getLoginMethod,
|
||||
registerLoginMethod,
|
||||
registerSecondFactor,
|
||||
} from "../auth/registry.js";
|
||||
import type { VerifiedIdentity } from "../auth/types.js";
|
||||
import type {
|
||||
MutableConnectConfig,
|
||||
SshAuthProvider,
|
||||
@@ -392,6 +398,60 @@ export function createPluginSsh({ manifest, bag, audit }: Deps): PluginSsh {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns what a plugin method returned into core's identity. Only the fields
|
||||
* the SDK documents are copied, so a plugin cannot set core-only extras, and
|
||||
* its rate limit key is kept apart from core's and other plugins'.
|
||||
*/
|
||||
export function toCoreIdentity(
|
||||
pluginId: string,
|
||||
identity: PluginVerifiedIdentity,
|
||||
): VerifiedIdentity {
|
||||
if (identity.kind === "user") {
|
||||
return {
|
||||
kind: "user",
|
||||
userId: identity.userId,
|
||||
mfaSatisfied: identity.mfaSatisfied,
|
||||
password: identity.password,
|
||||
returnTo: identity.returnTo,
|
||||
rememberMe: identity.rememberMe,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "external",
|
||||
provider: identity.provider,
|
||||
subject: identity.subject,
|
||||
email: identity.email,
|
||||
name: identity.name,
|
||||
groups: identity.groups,
|
||||
isAdmin: identity.isAdmin,
|
||||
allowedUsers: identity.allowedUsers,
|
||||
mfaSatisfied: identity.mfaSatisfied,
|
||||
returnTo: identity.returnTo,
|
||||
rememberMe: identity.rememberMe,
|
||||
legacyIdentifier: identity.legacy?.identifier,
|
||||
ssoProviderId:
|
||||
identity.logoutClaims?.providerId ??
|
||||
identity.legacy?.providerRowId ??
|
||||
null,
|
||||
oidcSub: identity.logoutClaims?.sub ?? null,
|
||||
oidcSid: identity.logoutClaims?.sid ?? null,
|
||||
roleSync: identity.roles
|
||||
? {
|
||||
desired: [...identity.roles.desired],
|
||||
managed: [...identity.roles.managed],
|
||||
}
|
||||
: undefined,
|
||||
rateLimitUsername: identity.rateLimitKey
|
||||
? rateLimitKeyFor(pluginId, identity.rateLimitKey)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function rateLimitKeyFor(pluginId: string, key: string): string {
|
||||
return `plugin:${pluginId}:${key}`;
|
||||
}
|
||||
|
||||
export function createPluginAuth({ manifest, bag, audit }: Deps): PluginAuth {
|
||||
const pluginId = manifest.id;
|
||||
const declared = manifest.capabilities;
|
||||
@@ -477,13 +537,16 @@ export function createPluginAuth({ manifest, bag, audit }: Deps): PluginAuth {
|
||||
callback: method.callback
|
||||
? async (...args) => {
|
||||
await granted();
|
||||
return method.callback!(...args);
|
||||
return toCoreIdentity(
|
||||
pluginId,
|
||||
await method.callback!(...args),
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
verify: method.verify
|
||||
? async (...args) => {
|
||||
await granted();
|
||||
return method.verify!(...args);
|
||||
return toCoreIdentity(pluginId, await method.verify!(...args));
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
@@ -550,5 +613,61 @@ export function createPluginAuth({ manifest, bag, audit }: Deps): PluginAuth {
|
||||
success: true,
|
||||
});
|
||||
},
|
||||
|
||||
completeRedirectLogin: async (methodId, req, res) => {
|
||||
requireDeclared(contributes.loginMethods, methodId, "loginMethods");
|
||||
await granted();
|
||||
const method = getLoginMethod(methodId);
|
||||
if (!method || method.pluginId !== pluginId) {
|
||||
throw new Error(
|
||||
`Plugin ${pluginId} has no registered login method "${methodId}"`,
|
||||
);
|
||||
}
|
||||
const { handleRedirectCallback } =
|
||||
await import("../database/routes/auth-routes.js");
|
||||
await handleRedirectCallback(method, req as never, res as never);
|
||||
},
|
||||
|
||||
revokeSessions: async (match) => {
|
||||
await granted();
|
||||
if (!match.sub && !match.sid) return 0;
|
||||
const { AuthManager } = await import("../utils/auth-manager.js");
|
||||
const revoked = await AuthManager.getInstance().revokeSessionsByOidc({
|
||||
ssoProviderId: match.providerId ?? null,
|
||||
sub: match.sub ?? null,
|
||||
sid: match.sid ?? null,
|
||||
});
|
||||
await audit(
|
||||
"auth_sessions_revoked",
|
||||
`${revoked} session(s) for provider ${match.providerId ?? "none"}`,
|
||||
{ success: true },
|
||||
);
|
||||
return revoked;
|
||||
},
|
||||
|
||||
loginRateLimit: {
|
||||
isLocked: async (ip, key) => {
|
||||
await granted();
|
||||
const { loginRateLimiter } =
|
||||
await import("../utils/login-rate-limiter.js");
|
||||
return loginRateLimiter.isLocked(ip, rateLimitKeyFor(pluginId, key));
|
||||
},
|
||||
recordFailure: async (ip, key) => {
|
||||
await granted();
|
||||
const { loginRateLimiter } =
|
||||
await import("../utils/login-rate-limiter.js");
|
||||
loginRateLimiter.recordFailedAttempt(
|
||||
ip,
|
||||
rateLimitKeyFor(pluginId, key),
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
countLinkedUsers: async (provider) => {
|
||||
await granted();
|
||||
const { createCurrentUserAuthRepository } =
|
||||
await import("../database/repositories/factory.js");
|
||||
return createCurrentUserAuthRepository().countUsersForProvider(provider);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import { pluginLogger } from "../utils/logger.js";
|
||||
import { getRequestBaseUrlWithForceHTTPS } from "../utils/request-origin.js";
|
||||
import { pluginEvents } from "./events.js";
|
||||
import * as registry from "./registry.js";
|
||||
import * as serviceRegistry from "./service-registry.js";
|
||||
@@ -683,6 +684,10 @@ export function createPluginContext(
|
||||
);
|
||||
return router as never;
|
||||
},
|
||||
baseUrl: (req) =>
|
||||
getRequestBaseUrlWithForceHTTPS(
|
||||
req as Parameters<typeof getRequestBaseUrlWithForceHTTPS>[0],
|
||||
),
|
||||
},
|
||||
|
||||
ws: {
|
||||
|
||||
@@ -343,7 +343,13 @@ export class PluginLoader {
|
||||
// than leave it half-running against a schema that is not there. A
|
||||
// throw here lands in the catch below, which fails this plugin only.
|
||||
const { migratePlugin } = await import("./data.js");
|
||||
await migratePlugin(plugin.id, plugin.dir);
|
||||
const applied = await migratePlugin(plugin.id, plugin.dir);
|
||||
// A new table may be where core data is waiting to move.
|
||||
if (applied.length > 0) {
|
||||
const { runPluginDataMoves } =
|
||||
await import("../utils/crypto-migration/plugin-data-moves.js");
|
||||
await runPluginDataMoves();
|
||||
}
|
||||
|
||||
const handle = createPluginHandle(plugin.id, { activate, deactivate });
|
||||
const ctx = createPluginContext(plugin.manifest, handle);
|
||||
|
||||
Reference in new issue
Block a user