mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: move totp and passkeys into plugins (C1)
This commit is contained in:
1 parent
f4f26a9b8b
commit
5c18435105
235 files changed
+22545
-5610
No files matched your search
@@ -24,6 +24,8 @@ interface ActorStore {
|
||||
userId: string;
|
||||
/** Where the identity came from, for the audit line. */
|
||||
source: "request" | "asUser" | "service";
|
||||
/** The session a request came in on, so core can spare it when revoking. */
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
const storage = new AsyncLocalStorage<ActorStore>();
|
||||
@@ -32,8 +34,13 @@ export function runAsActor<T>(
|
||||
userId: string,
|
||||
source: ActorStore["source"],
|
||||
fn: () => T,
|
||||
sessionId?: string,
|
||||
): T {
|
||||
return storage.run({ userId, source }, fn);
|
||||
return storage.run({ userId, source, sessionId }, fn);
|
||||
}
|
||||
|
||||
export function getActorSessionId(): string | undefined {
|
||||
return storage.getStore()?.sessionId;
|
||||
}
|
||||
|
||||
export function getActor(): string | undefined {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
PluginSshInteractionError,
|
||||
} from "@termix/plugin-sdk/backend";
|
||||
import { assertCapability } from "./permissions.js";
|
||||
import { getActor } from "./actor.js";
|
||||
import { getActor, getActorSessionId } from "./actor.js";
|
||||
import type { DisposableBag } from "./disposables.js";
|
||||
import {
|
||||
getSshAuthProvider,
|
||||
@@ -511,13 +511,26 @@ export function createPluginAuth({ manifest, bag, audit }: Deps): PluginAuth {
|
||||
recordEnrollment: async (userId, factorId) => {
|
||||
requireDeclared(contributes.secondFactors, factorId, "secondFactors");
|
||||
await granted();
|
||||
const { createCurrentUserAuthRepository } =
|
||||
await import("../database/repositories/factory.js");
|
||||
const { assertSecondFactorEnrollmentAllowed } =
|
||||
await import("../auth/core-auth.js");
|
||||
assertSecondFactorEnrollmentAllowed();
|
||||
const {
|
||||
createCurrentSessionRepository,
|
||||
createCurrentTrustedDeviceRepository,
|
||||
createCurrentUserAuthRepository,
|
||||
} = await import("../database/repositories/factory.js");
|
||||
await createCurrentUserAuthRepository().recordSecondFactor(
|
||||
userId,
|
||||
pluginId,
|
||||
factorId,
|
||||
);
|
||||
// A new factor signs the user out everywhere else and forgets trusted
|
||||
// devices, so nothing that skipped it before keeps skipping it.
|
||||
await createCurrentSessionRepository().revokeAllForUser(
|
||||
userId,
|
||||
getActor() === userId ? getActorSessionId() : undefined,
|
||||
);
|
||||
await createCurrentTrustedDeviceRepository().deleteByUserId(userId);
|
||||
await audit("auth_factor_enrolled", `${factorId} for ${userId}`, {
|
||||
success: true,
|
||||
});
|
||||
|
||||
@@ -600,6 +600,26 @@ export function createPluginContext(
|
||||
{ action: "secret_delete", details: () => "deleted a secret" },
|
||||
),
|
||||
|
||||
seal: async (value) => {
|
||||
await assertCapability(pluginId, "secrets:own", declared);
|
||||
const { encryptSystemSecret } =
|
||||
await import("../utils/system-secret-crypto.js");
|
||||
return encryptSystemSecret(value);
|
||||
},
|
||||
|
||||
unseal: async (sealed) => {
|
||||
await assertCapability(pluginId, "secrets:own", declared);
|
||||
const { decryptSystemSecret, isSystemEncrypted } =
|
||||
await import("../utils/system-secret-crypto.js");
|
||||
// Only values seal() wrote: a plain string must not pass as sealed.
|
||||
if (!sealed || !isSystemEncrypted(sealed)) return null;
|
||||
try {
|
||||
return await decryptSystemSecret(sealed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
offer: (key, resolve) => {
|
||||
const entry = manifest.providesSecret?.find(
|
||||
(candidate) => candidate.key === key,
|
||||
|
||||
@@ -204,12 +204,15 @@ export function createPluginRouter({
|
||||
// After auth, so the actor is the user core authenticated rather than
|
||||
// whatever the request claimed.
|
||||
outer.use((req: Request, _res: Response, next: NextFunction) => {
|
||||
const userId = (req as Request & { userId?: string }).userId;
|
||||
const { userId, sessionId } = req as Request & {
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
if (!userId) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
runAsActor(userId, "request", () => next());
|
||||
runAsActor(userId, "request", () => next(), sessionId);
|
||||
});
|
||||
|
||||
outer.use(inner);
|
||||
|
||||
Reference in new issue
Block a user