Record the roles run on the live server, and drop the mock's made-up permission
A throwaway role on the production server confirmed the create shape, one-pointer changes to permissions, bases and name together, the grant refusal, and the in-use refusal when another role builds on it. It also showed that a permission name Stalwart does not know fails the whole update -- which is how jmapEmailSet, carried by the mock since Accounts was built, turned out not to exist. The mock uses jmapEmailUpdate and now refuses unknown names against the 0.16.22 snapshot. Some permissions an administrator holds are never listed by /api/account (sysLogCreate was granted without complaint), so the picker locks their Allow; KNOWN-ISSUES says so.
This commit is contained in:
@@ -248,3 +248,10 @@ test("the default roles are read from the authentication settings", () => {
|
||||
assert.deepEqual(list[0]!.defaultUserRoleIds, { r1: true });
|
||||
assert.throws(() => make("tenant-admin").handlers["x:Authentication/get"]!({}), (e: Refused) => e.type === "forbidden");
|
||||
});
|
||||
|
||||
test("a permission name Stalwart does not know fails the whole change", () => {
|
||||
const dir = make("admin");
|
||||
const r = dir.handlers["x:Role/set"]!({ update: { r4: { "enabledPermissions/notARealPermission": true, description: "Renamed" } } }) as { notUpdated?: Record<string, { type: string; properties: string[] }> };
|
||||
assert.equal(r.notUpdated?.r4?.type, "invalidPatch");
|
||||
assert.deepEqual(r.notUpdated!.r4!.properties, ["enabledPermissions/notARealPermission"]);
|
||||
});
|
||||
|
||||
@@ -27,8 +27,15 @@
|
||||
* delete them) or `user`.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
type Obj = Record<string, unknown>;
|
||||
|
||||
/** Every permission Stalwart 0.16.22 knows, from the snapshot the translations are checked against. */
|
||||
const KNOWN_PERMISSIONS = new Set(
|
||||
(JSON.parse(readFileSync(new URL("../../../web/src/locales/permissions/source.json", import.meta.url), "utf8")) as { permissions: Array<{ name: string }> }).permissions.map((p) => p.name),
|
||||
);
|
||||
|
||||
export type MockRole = "admin" | "tenant-admin" | "helpdesk" | "user";
|
||||
|
||||
const OPS = ["Get", "Query", "Create", "Update", "Destroy"] as const;
|
||||
@@ -38,7 +45,7 @@ const all = (...objects: string[]) => objects.flatMap((o) => OPS.map((op) => `sy
|
||||
const READ_SERVER = ["sysQueuedMessageGet", "sysQueuedMessageQuery", "sysMetricGet", "sysMetricQuery"];
|
||||
|
||||
/** A few of the ordinary ones, so the list looks like what a server sends. */
|
||||
const USER_PERMISSIONS = ["jmapEmailGet", "jmapEmailSet", "jmapMailboxGet", "sysAccountSettingsGet"];
|
||||
const USER_PERMISSIONS = ["jmapEmailGet", "jmapEmailUpdate", "jmapMailboxGet", "sysAccountSettingsGet"];
|
||||
|
||||
export function permissionsFor(role: MockRole): string[] {
|
||||
switch (role) {
|
||||
@@ -136,7 +143,7 @@ export function createDirectory(opts: Options) {
|
||||
{ id: "r1", description: "User", enabledPermissions: flags(USER_PERMISSIONS), disabledPermissions: {}, roleIds: {}, memberTenantId: null },
|
||||
{ id: "r2", description: "Helpdesk", enabledPermissions: flags(permissionsFor("helpdesk").filter((p) => p.startsWith("sys"))), disabledPermissions: {}, roleIds: { r1: true } },
|
||||
{ id: "r3", description: "Directory manager", enabledPermissions: flags(all("Account")), disabledPermissions: {}, roleIds: { r1: true } },
|
||||
{ id: "r4", description: "Read-only auditor", enabledPermissions: flags(["sysAccountGet", "sysAccountQuery", "sysDomainGet", "sysDomainQuery", "sysLogGet"]), disabledPermissions: flags(["jmapEmailSet"]), roleIds: { r1: true } },
|
||||
{ id: "r4", description: "Read-only auditor", enabledPermissions: flags(["sysAccountGet", "sysAccountQuery", "sysDomainGet", "sysDomainQuery", "sysLogGet"]), disabledPermissions: flags(["jmapEmailUpdate"]), roleIds: { r1: true } },
|
||||
];
|
||||
/** Stalwart's defaults: which roles an account gets when it is given no others. */
|
||||
const authentication: Record<string, Obj> = { defaultUserRoleIds: { r1: true }, defaultGroupRoleIds: {}, defaultTenantRoleIds: {}, defaultAdminRoleIds: {} };
|
||||
@@ -549,6 +556,11 @@ export function createDirectory(opts: Options) {
|
||||
return !!r && Object.keys((r.roleIds as Obj) ?? {}).every(walk);
|
||||
};
|
||||
if (!Object.keys((o.roleIds as Obj) ?? {}).every(walk)) return setError("invalidProperties", "A role cannot inherit from itself or from a role that does not exist.", ["roleIds"]);
|
||||
// A name that is not a permission fails the whole change, as the live server does.
|
||||
for (const set of ["enabledPermissions", "disabledPermissions"]) {
|
||||
const bad = Object.keys((o[set] as Obj) ?? {}).find((p) => !KNOWN_PERMISSIONS.has(p));
|
||||
if (bad) return setError("invalidProperties", "Invalid value for object property", [`${set}/${bad}`]);
|
||||
}
|
||||
const granted = new Set(Object.keys((o.enabledPermissions as Obj) ?? {}));
|
||||
for (const rid of seen) for (const p of Object.keys((roles_(rid)!.enabledPermissions as Obj) ?? {})) granted.add(p);
|
||||
const missing = [...granted].filter((p) => !permissions.has(p));
|
||||
|
||||
Reference in New Issue
Block a user