UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

946 lines (791 loc) 29.9 kB
import { randomUUID } from "node:crypto"; import { Alepha } from "alepha"; import { $action, AlephaServer, ForbiddenError, UnauthorizedError, } from "alepha/server"; import { describe, expect, it } from "vitest"; import { $issuer, $secure, AlephaSecurity } from "../index.ts"; // ----------------------------------------------------------------------------------------------------------------- // Shared setup // ----------------------------------------------------------------------------------------------------------------- function setup() { class TestApp { main = $issuer({ secret: "test-main", roles: [ { name: "admin", permissions: [{ name: "*" }], }, { name: "editor", permissions: [ { name: "posts:read" }, { name: "posts:create" }, { name: "posts:update" }, ], }, { name: "viewer", permissions: [{ name: "posts:read" }], }, { name: "moderator", permissions: [ { name: "posts:read" }, { name: "posts:delete" }, { name: "comments:delete" }, ], }, ], }); external = $issuer({ secret: "test-external", roles: [ { name: "partner", permissions: [{ name: "posts:read" }], }, ], }); // --------------------------------------------------------------------------------------------------------------- // Actions with single options // --------------------------------------------------------------------------------------------------------------- authOnly = $action({ use: [$secure()], handler: () => "auth-only", }); requireViewer = $action({ use: [$secure({ roles: ["viewer"] })], handler: () => "viewer", }); requireRead = $action({ use: [$secure({ permissions: ["posts:read"] })], handler: () => "read", }); requireMainIssuer = $action({ use: [$secure({ issuers: ["main"] })], handler: () => "main-issuer", }); // --------------------------------------------------------------------------------------------------------------- // Actions with two-option combinations // --------------------------------------------------------------------------------------------------------------- issuerAndRole = $action({ use: [$secure({ issuers: ["main"], roles: ["editor"] })], handler: () => "issuer+role", }); issuerAndPermission = $action({ use: [$secure({ issuers: ["main"], permissions: ["posts:create"] })], handler: () => "issuer+permission", }); issuerAndGuard = $action({ use: [ $secure({ issuers: ["main"], guard: (user) => user.email === "allowed@test.com", }), ], handler: () => "issuer+guard", }); roleAndPermission = $action({ use: [$secure({ roles: ["editor"], permissions: ["posts:create"] })], handler: () => "role+permission", }); roleAndGuard = $action({ use: [ $secure({ roles: ["editor"], guard: (user) => user.email === "allowed@test.com", }), ], handler: () => "role+guard", }); permissionAndGuard = $action({ use: [ $secure({ permissions: ["posts:read"], guard: (user) => user.email === "allowed@test.com", }), ], handler: () => "permission+guard", }); // --------------------------------------------------------------------------------------------------------------- // Actions with three-option combinations // --------------------------------------------------------------------------------------------------------------- issuerRolePermission = $action({ use: [ $secure({ issuers: ["main"], roles: ["editor"], permissions: ["posts:create"], }), ], handler: () => "issuer+role+permission", }); issuerRoleGuard = $action({ use: [ $secure({ issuers: ["main"], roles: ["editor"], guard: (user) => user.email === "allowed@test.com", }), ], handler: () => "issuer+role+guard", }); issuerPermissionGuard = $action({ use: [ $secure({ issuers: ["main"], permissions: ["posts:create"], guard: (user) => user.email === "allowed@test.com", }), ], handler: () => "issuer+permission+guard", }); rolePermissionGuard = $action({ use: [ $secure({ roles: ["editor"], permissions: ["posts:create"], guard: (user) => user.email === "allowed@test.com", }), ], handler: () => "role+permission+guard", }); // --------------------------------------------------------------------------------------------------------------- // All four options // --------------------------------------------------------------------------------------------------------------- allOptions = $action({ use: [ $secure({ issuers: ["main"], roles: ["editor"], permissions: ["posts:create"], guard: (user) => user.email === "allowed@test.com", }), ], handler: () => "all", }); // --------------------------------------------------------------------------------------------------------------- // Multiple permissions (AND logic) // --------------------------------------------------------------------------------------------------------------- multiplePermissions = $action({ use: [$secure({ permissions: ["posts:read", "posts:delete"] })], handler: () => "multi-perm", }); // --------------------------------------------------------------------------------------------------------------- // Multiple issuers (OR logic) // --------------------------------------------------------------------------------------------------------------- multipleIssuers = $action({ use: [$secure({ issuers: ["main", "external"] })], handler: () => "multi-issuer", }); // --------------------------------------------------------------------------------------------------------------- // Multiple roles (OR logic) // --------------------------------------------------------------------------------------------------------------- multipleRoles = $action({ use: [$secure({ roles: ["editor", "moderator"] })], handler: () => "multi-role", }); } const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity); const app = alepha.inject(TestApp); const users = { admin: { id: randomUUID(), roles: ["admin"], realm: "main", email: "admin@test.com", name: "Admin", }, editor: { id: randomUUID(), roles: ["editor"], realm: "main", email: "allowed@test.com", name: "Editor", }, editorWrongEmail: { id: randomUUID(), roles: ["editor"], realm: "main", email: "other@test.com", name: "Editor2", }, viewer: { id: randomUUID(), roles: ["viewer"], realm: "main", email: "viewer@test.com", name: "Viewer", }, moderator: { id: randomUUID(), roles: ["moderator"], realm: "main", email: "mod@test.com", name: "Moderator", }, partner: { id: randomUUID(), roles: ["partner"], realm: "external", email: "partner@test.com", name: "Partner", }, noRoles: { id: randomUUID(), roles: [] as string[], realm: "main", email: "noroles@test.com", name: "NoRoles", }, noRealm: { id: randomUUID(), roles: ["editor"], email: "norealm@test.com", name: "NoRealm", }, editorAndModerator: { id: randomUUID(), roles: ["editor", "moderator"], realm: "main", email: "allowed@test.com", name: "EditorMod", }, }; return { alepha, app, users }; } // ----------------------------------------------------------------------------------------------------------------- // Two-option combinations // ----------------------------------------------------------------------------------------------------------------- describe("$secure combinations", () => { describe("issuers + roles", () => { it("should allow when both issuer and role match", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.issuerAndRole.run({}, { user: users.editor })).toBe( "issuer+role", ); }); it("should deny when issuer matches but role does not", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.issuerAndRole.run({}, { user: users.viewer }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when role matches but issuer does not", async () => { const { alepha, app, users } = setup(); await alepha.start(); const externalEditor = { ...users.editor, id: randomUUID(), realm: "external", }; await expect( app.issuerAndRole.run({}, { user: externalEditor }), ).rejects.toThrowError(ForbiddenError); }); }); describe("issuers + permissions", () => { it("should allow when both issuer and permission match", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect( await app.issuerAndPermission.run({}, { user: users.editor }), ).toBe("issuer+permission"); }); it("should deny when issuer matches but permission does not", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.issuerAndPermission.run({}, { user: users.viewer }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when permission would match but issuer does not", async () => { const { alepha, app, users } = setup(); await alepha.start(); const externalEditor = { ...users.editor, id: randomUUID(), realm: "external", }; await expect( app.issuerAndPermission.run({}, { user: externalEditor }), ).rejects.toThrowError(ForbiddenError); }); }); describe("issuers + guard", () => { it("should allow when both issuer and guard pass", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.issuerAndGuard.run({}, { user: users.editor })).toBe( "issuer+guard", ); }); it("should deny when issuer matches but guard fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.issuerAndGuard.run({}, { user: users.editorWrongEmail }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when guard would pass but issuer does not", async () => { const { alepha, app, users } = setup(); await alepha.start(); const externalAllowed = { ...users.editor, id: randomUUID(), realm: "external", }; await expect( app.issuerAndGuard.run({}, { user: externalAllowed }), ).rejects.toThrowError(ForbiddenError); }); }); describe("roles + permissions", () => { it("should allow when both role and permission match", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.roleAndPermission.run({}, { user: users.editor })).toBe( "role+permission", ); }); it("should deny when role matches but permission does not", async () => { const { alepha, app, users } = setup(); await alepha.start(); // viewer role doesn't have posts:create, but we need a user WITH editor role but WITHOUT the permission // Actually: editor role has posts:create. So use a viewer who somehow has editor role name... no. // The check is: roles check is OR on role NAME, permissions check is on role PERMISSIONS. // A user with roles: ["editor"] has posts:create. A user with roles: ["viewer"] does NOT have posts:create. // But viewer also doesn't have the "editor" role name. So both checks fail. // To test "role matches but permission doesn't": we'd need a role that exists by name but lacks the permission. // viewer role exists, has posts:read, but NOT posts:create. // So: $secure({ roles: ["viewer"], permissions: ["posts:create"] }) with a viewer user would fail on permission. // Let's just verify viewer fails on roleAndPermission (requires editor role) await expect( app.roleAndPermission.run({}, { user: users.viewer }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when user has no roles", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.roleAndPermission.run({}, { user: users.noRoles }), ).rejects.toThrowError(ForbiddenError); }); }); describe("roles + guard", () => { it("should allow when both role and guard pass", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.roleAndGuard.run({}, { user: users.editor })).toBe( "role+guard", ); }); it("should deny when role matches but guard fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.roleAndGuard.run({}, { user: users.editorWrongEmail }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when guard would pass but role does not", async () => { const { alepha, app, users } = setup(); await alepha.start(); // viewer with the "allowed" email — guard would pass, but role check fails const viewerAllowed = { ...users.viewer, email: "allowed@test.com" }; await expect( app.roleAndGuard.run({}, { user: viewerAllowed }), ).rejects.toThrowError(ForbiddenError); }); }); describe("permissions + guard", () => { it("should allow when both permission and guard pass", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.permissionAndGuard.run({}, { user: users.editor })).toBe( "permission+guard", ); }); it("should deny when permission matches but guard fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); // viewer has posts:read but wrong email await expect( app.permissionAndGuard.run({}, { user: users.viewer }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when guard would pass but permission does not", async () => { const { alepha, app, users } = setup(); await alepha.start(); // user with correct email but no posts:read permission const noPermsAllowed = { ...users.noRoles, email: "allowed@test.com" }; await expect( app.permissionAndGuard.run({}, { user: noPermsAllowed }), ).rejects.toThrowError(ForbiddenError); }); }); }); // ----------------------------------------------------------------------------------------------------------------- // Three-option combinations // ----------------------------------------------------------------------------------------------------------------- describe("$secure three-option combinations", () => { describe("issuers + roles + permissions", () => { it("should allow when all three match", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect( await app.issuerRolePermission.run({}, { user: users.editor }), ).toBe("issuer+role+permission"); }); it("should deny when issuer fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); const externalEditor = { ...users.editor, id: randomUUID(), realm: "external", }; await expect( app.issuerRolePermission.run({}, { user: externalEditor }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when role fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.issuerRolePermission.run({}, { user: users.viewer }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when permission fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); // moderator role doesn't have posts:create await expect( app.issuerRolePermission.run({}, { user: users.moderator }), ).rejects.toThrowError(ForbiddenError); }); }); describe("issuers + roles + guard", () => { it("should allow when all three match", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.issuerRoleGuard.run({}, { user: users.editor })).toBe( "issuer+role+guard", ); }); it("should deny when guard fails (issuer and role pass)", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.issuerRoleGuard.run({}, { user: users.editorWrongEmail }), ).rejects.toThrowError(ForbiddenError); }); }); describe("issuers + permissions + guard", () => { it("should allow when all three match", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect( await app.issuerPermissionGuard.run({}, { user: users.editor }), ).toBe("issuer+permission+guard"); }); it("should deny when permission fails (issuer and guard would pass)", async () => { const { alepha, app, users } = setup(); await alepha.start(); // viewer has posts:read but not posts:create const viewerAllowed = { ...users.viewer, email: "allowed@test.com" }; await expect( app.issuerPermissionGuard.run({}, { user: viewerAllowed }), ).rejects.toThrowError(ForbiddenError); }); }); describe("roles + permissions + guard", () => { it("should allow when all three match", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect( await app.rolePermissionGuard.run({}, { user: users.editor }), ).toBe("role+permission+guard"); }); it("should deny when only guard fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.rolePermissionGuard.run({}, { user: users.editorWrongEmail }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when only role fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); // admin has all permissions and correct email but not "editor" role const adminAllowed = { ...users.admin, email: "allowed@test.com" }; await expect( app.rolePermissionGuard.run({}, { user: adminAllowed }), ).rejects.toThrowError(ForbiddenError); }); }); }); // ----------------------------------------------------------------------------------------------------------------- // All four options // ----------------------------------------------------------------------------------------------------------------- describe("$secure all options combined", () => { it("should allow when issuer + role + permission + guard all pass", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.allOptions.run({}, { user: users.editor })).toBe("all"); }); it("should deny when only issuer fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); const externalEditor = { ...users.editor, id: randomUUID(), realm: "external", }; await expect( app.allOptions.run({}, { user: externalEditor }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when only role fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); const viewerAllowed = { ...users.viewer, email: "allowed@test.com" }; await expect( app.allOptions.run({}, { user: viewerAllowed }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when only permission fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); // moderator role has posts:delete and comments:delete, but not posts:create const modAllowed = { ...users.moderator, roles: ["moderator"], email: "allowed@test.com", }; await expect( app.allOptions.run({}, { user: modAllowed }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when only guard fails", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.allOptions.run({}, { user: users.editorWrongEmail }), ).rejects.toThrowError(ForbiddenError); }); it("should deny unauthenticated user", async () => { const { alepha, app } = setup(); await alepha.start(); await expect(app.allOptions.run({})).rejects.toThrowError( UnauthorizedError, ); }); }); // ----------------------------------------------------------------------------------------------------------------- // Multiple permissions (AND logic) // ----------------------------------------------------------------------------------------------------------------- describe("$secure multiple permissions (AND)", () => { it("should allow when user has all required permissions", async () => { const { alepha, app, users } = setup(); await alepha.start(); // moderator has posts:read AND posts:delete expect( await app.multiplePermissions.run({}, { user: users.moderator }), ).toBe("multi-perm"); }); it("should deny when user has only some of the required permissions", async () => { const { alepha, app, users } = setup(); await alepha.start(); // viewer has posts:read but NOT posts:delete await expect( app.multiplePermissions.run({}, { user: users.viewer }), ).rejects.toThrowError(ForbiddenError); }); it("should deny when user has none of the required permissions", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.multiplePermissions.run({}, { user: users.noRoles }), ).rejects.toThrowError(ForbiddenError); }); it("should allow admin with wildcard permission", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.multiplePermissions.run({}, { user: users.admin })).toBe( "multi-perm", ); }); }); // ----------------------------------------------------------------------------------------------------------------- // Multiple issuers (OR logic) // ----------------------------------------------------------------------------------------------------------------- describe("$secure multiple issuers (OR)", () => { it("should allow user from first issuer", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.multipleIssuers.run({}, { user: users.editor })).toBe( "multi-issuer", ); }); it("should allow user from second issuer", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.multipleIssuers.run({}, { user: users.partner })).toBe( "multi-issuer", ); }); it("should deny user from unknown issuer", async () => { const { alepha, app } = setup(); await alepha.start(); const unknownRealmUser = { id: randomUUID(), roles: ["admin"], realm: "unknown", name: "Unknown", }; await expect( app.multipleIssuers.run({}, { user: unknownRealmUser }), ).rejects.toThrowError(ForbiddenError); }); }); // ----------------------------------------------------------------------------------------------------------------- // Multiple roles (OR logic) // ----------------------------------------------------------------------------------------------------------------- describe("$secure multiple roles (OR)", () => { it("should allow user with first matching role", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.multipleRoles.run({}, { user: users.editor })).toBe( "multi-role", ); }); it("should allow user with second matching role", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.multipleRoles.run({}, { user: users.moderator })).toBe( "multi-role", ); }); it("should allow user with both roles", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect( await app.multipleRoles.run({}, { user: users.editorAndModerator }), ).toBe("multi-role"); }); it("should deny user with none of the required roles", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.multipleRoles.run({}, { user: users.viewer }), ).rejects.toThrowError(ForbiddenError); }); }); // ----------------------------------------------------------------------------------------------------------------- // Check order: issuer → role → permission → guard // ----------------------------------------------------------------------------------------------------------------- describe("$secure check order", () => { it("should check issuer before role", async () => { const { alepha, app, users } = setup(); await alepha.start(); // User from wrong issuer with wrong role — error message should be about issuer, not role const wrongBoth = { id: randomUUID(), roles: ["viewer"], realm: "external", name: "Wrong", }; await expect( app.issuerAndRole.run({}, { user: wrongBoth }), ).rejects.toThrowError(/issuer/i); }); it("should check role before permission", async () => { const { alepha, app, users } = setup(); await alepha.start(); // User with wrong role and wrong permission — error should be about role await expect( app.roleAndPermission.run({}, { user: users.viewer }), ).rejects.toThrowError(/role.*required/i); }); it("should check permission before guard", async () => { const { alepha, app, users } = setup(); await alepha.start(); // User with correct email (guard would pass) but no permission const noPermsAllowed = { ...users.noRoles, email: "allowed@test.com" }; await expect( app.permissionAndGuard.run({}, { user: noPermsAllowed }), ).rejects.toThrowError(/permission.*required/i); }); it("should reach guard only after all other checks pass", async () => { const { alepha, app, users } = setup(); await alepha.start(); // Editor with wrong email — issuer/role/permission all pass, only guard fails await expect( app.allOptions.run({}, { user: users.editorWrongEmail }), ).rejects.toThrowError(/access denied/i); }); }); // ----------------------------------------------------------------------------------------------------------------- // Edge cases // ----------------------------------------------------------------------------------------------------------------- describe("$secure edge cases", () => { it("should deny user with empty roles array against role check", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.requireViewer.run({}, { user: users.noRoles }), ).rejects.toThrowError(ForbiddenError); }); it("should deny user with undefined roles against role check", async () => { const { alepha, app } = setup(); await alepha.start(); const noRolesUndefined = { id: randomUUID(), realm: "main", name: "NoRoles", }; await expect( app.requireViewer.run({}, { user: noRolesUndefined }), ).rejects.toThrowError(ForbiddenError); }); it("should deny user with no realm against issuer check", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.requireMainIssuer.run({}, { user: users.noRealm }), ).rejects.toThrowError(ForbiddenError); }); it("should deny user with empty roles array against permission check", async () => { const { alepha, app, users } = setup(); await alepha.start(); await expect( app.requireRead.run({}, { user: users.noRoles }), ).rejects.toThrowError(ForbiddenError); }); it("should allow auth-only action for user with no roles", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.authOnly.run({}, { user: users.noRoles })).toBe( "auth-only", ); }); it("should allow auth-only action for user with no realm", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.authOnly.run({}, { user: users.noRealm })).toBe( "auth-only", ); }); it("should allow admin wildcard through any permission check", async () => { const { alepha, app, users } = setup(); await alepha.start(); expect(await app.requireRead.run({}, { user: users.admin })).toBe("read"); }); });