mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: login policy hook for plugins and optional https redirect (#1584)
This commit is contained in:
1 parent
34a9c77114
commit
ad46f42ab1
53 files changed
+15250
-59
No files matched your search
@@ -12,6 +12,7 @@ const h = vi.hoisted(() => ({
|
||||
sessionId: undefined as string | undefined,
|
||||
revoked: [] as Array<{ userId: string; except?: string }>,
|
||||
trustedCleared: [] as string[],
|
||||
unrestricted: [] as string[],
|
||||
policyError: null as Error | null,
|
||||
logins: [] as Array<{ hostId: number; outcome: unknown }>,
|
||||
connectError: null as Error | null,
|
||||
@@ -126,6 +127,9 @@ vi.mock("../../database/repositories/factory.js", () => ({
|
||||
h.revoked.push({ userId, except });
|
||||
return 0;
|
||||
},
|
||||
clearRestrictionForUser: async (userId: string) => {
|
||||
h.unrestricted.push(userId);
|
||||
},
|
||||
}),
|
||||
createCurrentTrustedDeviceRepository: () => ({
|
||||
deleteByUserId: async (userId: string) => {
|
||||
@@ -145,6 +149,8 @@ const { getLoginMethod, getSecondFactor } =
|
||||
await import("../../auth/registry.js");
|
||||
const { PluginCapabilityError } =
|
||||
await import("@termix-ssh/plugin-sdk/backend");
|
||||
const { hasAuthPolicies, clearAuthPolicies } =
|
||||
await import("../../auth/policy.js");
|
||||
|
||||
function manifest(capabilities: string[], auth: Record<string, string[]> = {}) {
|
||||
return {
|
||||
@@ -655,6 +661,7 @@ describe("ctx.auth", () => {
|
||||
h.factors = [];
|
||||
h.revoked = [];
|
||||
h.trustedCleared = [];
|
||||
h.unrestricted = [];
|
||||
h.policyError = null;
|
||||
h.actor = "user-1";
|
||||
h.sessionId = "session-a";
|
||||
@@ -666,10 +673,65 @@ describe("ctx.auth", () => {
|
||||
await auth.recordEnrollment("user-1", "pin");
|
||||
expect(h.revoked).toEqual([{ userId: "user-1", except: "session-a" }]);
|
||||
expect(h.trustedCleared).toEqual(["user-1"]);
|
||||
expect(h.unrestricted).toEqual(["user-1"]);
|
||||
h.actor = undefined;
|
||||
h.sessionId = undefined;
|
||||
});
|
||||
|
||||
it("needs auth:policy for a login policy and drops it on deactivate", async () => {
|
||||
clearAuthPolicies();
|
||||
const plain = createPluginAuth({
|
||||
manifest: manifest(["auth:provide"]),
|
||||
bag: new DisposableBag("fixture"),
|
||||
audit: vi.fn(async () => {}),
|
||||
});
|
||||
expect(() => plain.policy.provide({ evaluate: () => ({}) })).toThrow(
|
||||
PluginCapabilityError,
|
||||
);
|
||||
|
||||
const bag = new DisposableBag("fixture");
|
||||
const audit = vi.fn(async () => {});
|
||||
const auth = createPluginAuth({
|
||||
manifest: manifest(["auth:policy"]),
|
||||
bag,
|
||||
audit,
|
||||
});
|
||||
const remove = auth.policy.provide({ evaluate: () => ({}) });
|
||||
expect(hasAuthPolicies()).toBe(true);
|
||||
expect(audit).toHaveBeenCalledWith(
|
||||
"auth_policy_provide",
|
||||
"fixture sets login rules",
|
||||
{ success: true },
|
||||
);
|
||||
remove();
|
||||
expect(hasAuthPolicies()).toBe(false);
|
||||
|
||||
auth.policy.provide({ evaluate: () => ({}) });
|
||||
await bag.disposeAll();
|
||||
expect(hasAuthPolicies()).toBe(false);
|
||||
});
|
||||
|
||||
it("lists second factors from every plugin", async () => {
|
||||
h.granted = new Set(["auth:provide"]);
|
||||
const bag = new DisposableBag("fixture");
|
||||
const auth = createPluginAuth({
|
||||
manifest: manifest(["auth:provide"], { secondFactors: ["pin"] }),
|
||||
bag,
|
||||
audit: vi.fn(async () => {}),
|
||||
});
|
||||
auth.registerSecondFactor({
|
||||
id: "pin",
|
||||
labelKey: "pin",
|
||||
isEnrolled: async () => false,
|
||||
verify: async () => true,
|
||||
});
|
||||
expect(auth.listSecondFactors()).toContainEqual({
|
||||
id: "pin",
|
||||
pluginId: "fixture",
|
||||
});
|
||||
await bag.disposeAll();
|
||||
});
|
||||
|
||||
it("refuses enrolment when core policy forbids second factors", async () => {
|
||||
h.granted = new Set(["auth:provide"]);
|
||||
h.factors = [];
|
||||
|
||||
@@ -71,6 +71,7 @@ const UNGATED: Record<string, string> = {
|
||||
"rbac.hasFor": "a yes/no about a role permission, grants nothing",
|
||||
"rbac.require": "route middleware for the plugin's own permissions",
|
||||
"capabilities.has": "a yes/no about the plugin's own grants",
|
||||
"auth.listSecondFactors": "ids of the second factors, no user data",
|
||||
"disposables.add": "the plugin's own cleanup",
|
||||
"schedule.every": "the plugin's own timers",
|
||||
"schedule.after": "the plugin's own timers",
|
||||
|
||||
Reference in new issue
Block a user