mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: namespace plugin permissions and keep them valid when a plugin is off
This commit is contained in:
1 parent
8431570ff7
commit
cc15559ea1
53 files changed
+31442
-351
No files matched your search
@@ -3,11 +3,37 @@ import {
|
||||
PERMISSION_CATALOG,
|
||||
getPermissionCatalog,
|
||||
isValidPermission,
|
||||
registerPermissionGroup,
|
||||
unregisterPermissionGroup,
|
||||
loadKnownPermissions,
|
||||
markPluginPermissionsDisabled,
|
||||
registerPluginPermissions,
|
||||
rememberPermissionGroup,
|
||||
resetPermissionCatalog,
|
||||
SYSTEM_ROLE_DEFAULTS,
|
||||
} from "../../utils/permission-catalog.js";
|
||||
|
||||
const GROUP = "testplugin";
|
||||
const PERMISSIONS = ["testplugin.view", "testplugin.manage"];
|
||||
|
||||
function register() {
|
||||
registerPluginPermissions({
|
||||
group: GROUP,
|
||||
pluginId: GROUP,
|
||||
label: "Test Plugin",
|
||||
icon: "Sparkles",
|
||||
permissions: PERMISSIONS,
|
||||
items: PERMISSIONS.map((permission) => ({
|
||||
permission,
|
||||
titleKey: "permissions.x.title",
|
||||
descriptionKey: "permissions.x.description",
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
describe("permission catalog", () => {
|
||||
afterEach(() => {
|
||||
resetPermissionCatalog();
|
||||
});
|
||||
|
||||
it("accepts every cataloged permission and group wildcard", () => {
|
||||
for (const entry of PERMISSION_CATALOG) {
|
||||
expect(isValidPermission(`${entry.group}.*`)).toBe(true);
|
||||
@@ -27,74 +53,126 @@ describe("permission catalog", () => {
|
||||
expect(isValidPermission("")).toBe(false);
|
||||
expect(isValidPermission("hosts")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runtime permission group lifecycle", () => {
|
||||
const group = "testplugin";
|
||||
const permissions = ["testplugin.view", "testplugin.manage"];
|
||||
|
||||
afterEach(() => {
|
||||
unregisterPermissionGroup(group);
|
||||
// Both moved out to their plugins in A5; the ids did not change, so no role
|
||||
// had to be migrated.
|
||||
it("no longer carries the ai or automations groups", () => {
|
||||
const groups = PERMISSION_CATALOG.map((entry) => entry.group);
|
||||
expect(groups).not.toContain("ai");
|
||||
expect(groups).not.toContain("automations");
|
||||
});
|
||||
|
||||
it("rejects a group's permissions before it is registered", () => {
|
||||
it("no longer seeds ai or automations onto the user role", () => {
|
||||
expect(SYSTEM_ROLE_DEFAULTS.user.permissions).not.toContain("ai.*");
|
||||
expect(SYSTEM_ROLE_DEFAULTS.user.permissions).not.toContain(
|
||||
"automations.*",
|
||||
);
|
||||
expect(SYSTEM_ROLE_DEFAULTS.user.permissions).toContain("hosts.*");
|
||||
});
|
||||
|
||||
it("gives every core group a label key for the role editor", () => {
|
||||
for (const entry of PERMISSION_CATALOG) {
|
||||
expect(entry.labelKey).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin permission lifecycle", () => {
|
||||
afterEach(() => {
|
||||
resetPermissionCatalog();
|
||||
});
|
||||
|
||||
it("rejects a plugin's permissions before it registers", () => {
|
||||
expect(isValidPermission("testplugin.view")).toBe(false);
|
||||
expect(isValidPermission("testplugin.*")).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts a group's permissions and wildcard once registered", () => {
|
||||
registerPermissionGroup({ group, permissions });
|
||||
it("accepts them once registered", () => {
|
||||
register();
|
||||
|
||||
expect(isValidPermission("testplugin.view")).toBe(true);
|
||||
expect(isValidPermission("testplugin.manage")).toBe(true);
|
||||
expect(isValidPermission("testplugin.*")).toBe(true);
|
||||
expect(isValidPermission("*")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a group's permissions again after it is unregistered", () => {
|
||||
registerPermissionGroup({ group, permissions });
|
||||
it("carries the plugin id, label and icon into the catalog", () => {
|
||||
register();
|
||||
|
||||
const entry = getPermissionCatalog().find((row) => row.group === GROUP);
|
||||
expect(entry?.pluginId).toBe(GROUP);
|
||||
expect(entry?.label).toBe("Test Plugin");
|
||||
expect(entry?.icon).toBe("Sparkles");
|
||||
expect(entry?.enabled).toBe(true);
|
||||
expect(entry?.items).toHaveLength(2);
|
||||
});
|
||||
|
||||
// The bug A5 fixes: unregistering made PUT /rbac/roles/:id reject every role
|
||||
// that still held one of these, so an admin could not save an unrelated
|
||||
// change while the plugin was off.
|
||||
it("keeps a disabled plugin's permissions valid", () => {
|
||||
register();
|
||||
markPluginPermissionsDisabled(GROUP);
|
||||
|
||||
expect(isValidPermission("testplugin.view")).toBe(true);
|
||||
|
||||
unregisterPermissionGroup(group);
|
||||
|
||||
expect(isValidPermission("testplugin.view")).toBe(false);
|
||||
expect(isValidPermission("testplugin.*")).toBe(false);
|
||||
expect(isValidPermission("testplugin.*")).toBe(true);
|
||||
});
|
||||
|
||||
it("includes registered groups in getPermissionCatalog and drops them on unregister", () => {
|
||||
expect(getPermissionCatalog().some((entry) => entry.group === group)).toBe(
|
||||
false,
|
||||
);
|
||||
it("marks a disabled plugin's group so the editor can grey it", () => {
|
||||
register();
|
||||
markPluginPermissionsDisabled(GROUP);
|
||||
|
||||
registerPermissionGroup({ group, permissions });
|
||||
expect(getPermissionCatalog().some((entry) => entry.group === group)).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
unregisterPermissionGroup(group);
|
||||
expect(getPermissionCatalog().some((entry) => entry.group === group)).toBe(
|
||||
false,
|
||||
);
|
||||
const entry = getPermissionCatalog().find((row) => row.group === GROUP);
|
||||
expect(entry?.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it("does not mutate the static PERMISSION_CATALOG array", () => {
|
||||
const originalLength = PERMISSION_CATALOG.length;
|
||||
registerPermissionGroup({ group, permissions });
|
||||
register();
|
||||
|
||||
expect(PERMISSION_CATALOG.length).toBe(originalLength);
|
||||
expect(PERMISSION_CATALOG.some((entry) => entry.group === group)).toBe(
|
||||
expect(PERMISSION_CATALOG.some((entry) => entry.group === GROUP)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("overwrites an existing runtime group when re-registered with the same name", () => {
|
||||
registerPermissionGroup({ group, permissions: ["testplugin.view"] });
|
||||
expect(isValidPermission("testplugin.manage")).toBe(false);
|
||||
it("lists a plugin group exactly once when re-registered", () => {
|
||||
register();
|
||||
register();
|
||||
|
||||
registerPermissionGroup({
|
||||
group,
|
||||
permissions: ["testplugin.view", "testplugin.manage"],
|
||||
});
|
||||
expect(isValidPermission("testplugin.manage")).toBe(true);
|
||||
const matches = getPermissionCatalog().filter(
|
||||
(entry) => entry.group === GROUP,
|
||||
);
|
||||
expect(matches).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("known permissions", () => {
|
||||
afterEach(() => {
|
||||
resetPermissionCatalog();
|
||||
});
|
||||
|
||||
// The upgrade path: a permission a role already holds stays grantable even
|
||||
// if no plugin ever registers it again.
|
||||
it("accepts a permission loaded from the record with no plugin present", () => {
|
||||
expect(isValidPermission("gone.use")).toBe(false);
|
||||
|
||||
loadKnownPermissions(["gone.use"]);
|
||||
|
||||
expect(isValidPermission("gone.use")).toBe(true);
|
||||
expect(isValidPermission("gone.*")).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts a group recorded from a wildcard nothing else named", () => {
|
||||
rememberPermissionGroup("legacy");
|
||||
|
||||
expect(isValidPermission("legacy.*")).toBe(true);
|
||||
// Still rejects a concrete permission nobody ever registered.
|
||||
expect(isValidPermission("legacy.invent")).toBe(false);
|
||||
});
|
||||
|
||||
it("still rejects a permission nobody has ever registered", () => {
|
||||
loadKnownPermissions(["gone.use"]);
|
||||
|
||||
expect(isValidPermission("madeup.use")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -120,11 +120,13 @@ describe("PermissionManager.hasPermission wildcard matching", () => {
|
||||
});
|
||||
|
||||
it("resolves wildcards for a runtime-registered permission group", async () => {
|
||||
const { registerPermissionGroup, unregisterPermissionGroup } =
|
||||
const { registerPluginPermissions, resetPermissionCatalog } =
|
||||
await import("../../utils/permission-catalog.js");
|
||||
|
||||
registerPermissionGroup({
|
||||
registerPluginPermissions({
|
||||
group: "testplugin",
|
||||
pluginId: "testplugin",
|
||||
label: "Test Plugin",
|
||||
permissions: ["testplugin.view", "testplugin.manage"],
|
||||
});
|
||||
|
||||
@@ -136,7 +138,7 @@ describe("PermissionManager.hasPermission wildcard matching", () => {
|
||||
withPermissions(["*"]);
|
||||
expect(await manager.hasPermission("u1", "testplugin.view")).toBe(true);
|
||||
} finally {
|
||||
unregisterPermissionGroup("testplugin");
|
||||
resetPermissionCatalog();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user