First Commit. Duplicate bunext-sqlite preset
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USER_TYPES } from "@/db/types/db";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import isPassThroughUser from "./is-pass-through-user";
|
||||
import { UserManagementUserTypes } from "@/src/dict/user-types-dict";
|
||||
|
||||
type Params = {
|
||||
user_types?: BUN_SQLITE_BSQLITE_USER_TYPES[] | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the actor may create new user records. User type assignment on
|
||||
* the new record is still limited by the assignment matrix.
|
||||
*/
|
||||
export default function canCreateUser({ user_types }: Params): boolean {
|
||||
if (isPassThroughUser({ user_types })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return checkUserAccess({
|
||||
user_types,
|
||||
includes: UserManagementUserTypes,
|
||||
}).success;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USER_TYPES } from "@/db/types/db";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import isPassThroughUser from "./is-pass-through-user";
|
||||
import { UserManagementUserTypes } from "@/src/dict/user-types-dict";
|
||||
|
||||
type Params = {
|
||||
user_types?: BUN_SQLITE_BSQLITE_USER_TYPES[] | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the actor may list (view) all user records, not just their
|
||||
* own. Pass-through actors always pass.
|
||||
*/
|
||||
export default function canListUsers({ user_types }: Params): boolean {
|
||||
if (isPassThroughUser({ user_types })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return checkUserAccess({
|
||||
user_types,
|
||||
includes: UserManagementUserTypes,
|
||||
}).success;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USER_TYPES } from "@/db/types/db";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import isPassThroughUser from "./is-pass-through-user";
|
||||
import { UserManagementUserTypes } from "@/src/dict/user-types-dict";
|
||||
|
||||
type Params = {
|
||||
user_types?: BUN_SQLITE_BSQLITE_USER_TYPES[] | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the actor may manage user type records (insert or delete rows
|
||||
* in the `user_types` table). Which individual types they may grant or
|
||||
* remove is still enforced per type by the assignment matrix.
|
||||
*/
|
||||
export default function canManageUserTypes({
|
||||
user_types,
|
||||
}: Params): boolean {
|
||||
if (isPassThroughUser({ user_types })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return checkUserAccess({
|
||||
user_types,
|
||||
includes: UserManagementUserTypes,
|
||||
}).success;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USER_TYPES } from "@/db/types/db";
|
||||
import type { UserType } from "@/src/types";
|
||||
import userTypeAssignment from "./user-type-assignment";
|
||||
|
||||
type Params = {
|
||||
user_types?: BUN_SQLITE_BSQLITE_USER_TYPES[] | null;
|
||||
target_user_types?: BUN_SQLITE_BSQLITE_USER_TYPES[] | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the actor may revoke a user (i.e. add the `revoked` user type).
|
||||
*
|
||||
* - A user holding the `super_admin` type can never be revoked.
|
||||
* - Otherwise governed by the assignment matrix for the `revoked` type:
|
||||
* pass-through actors (`super_admin`) may revoke anyone, and `admin`
|
||||
* may revoke users who do not hold an excluded protected type.
|
||||
*/
|
||||
export default function canRevokeUser({
|
||||
user_types,
|
||||
target_user_types,
|
||||
}: Params): boolean {
|
||||
if (target_user_types?.some((ut) => ut.user_type === "super_admin")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target_user_type_values: UserType[] | undefined = target_user_types
|
||||
?.map((ut) => ut.user_type)
|
||||
.filter((ut) => Boolean(ut)) as UserType[] | undefined;
|
||||
|
||||
return userTypeAssignment({
|
||||
actor_user_types: user_types,
|
||||
target_user_type: "revoked",
|
||||
target_user_types: target_user_type_values,
|
||||
action: "assign",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USER_TYPES } from "@/db/types/db";
|
||||
import type { UserType } from "@/src/types";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import isPassThroughUser from "./is-pass-through-user";
|
||||
|
||||
type Params = {
|
||||
actor_user_types?: BUN_SQLITE_BSQLITE_USER_TYPES[] | null;
|
||||
actor_user_id?: number | string;
|
||||
target_user_id?: number | string;
|
||||
target_user_types?: BUN_SQLITE_BSQLITE_USER_TYPES[] | null;
|
||||
/**
|
||||
* The keys of the fields being updated (e.g. ["first_name", "email"]).
|
||||
* Used to restrict contact fields on protected users.
|
||||
*/
|
||||
update_fields?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Fields treated as contact information.
|
||||
*/
|
||||
export const ContactUpdateFields = ["email"];
|
||||
|
||||
/**
|
||||
* User types whose contact fields (email) are protected from changes by
|
||||
* non-pass-through actors.
|
||||
*/
|
||||
export const ContactProtectedUserTypes: UserType[] = ["super_admin"];
|
||||
|
||||
/**
|
||||
* Whether the actor may update a user record.
|
||||
*
|
||||
* - Any user may update their own record (profile fields only; user type
|
||||
* changes are still governed by the assignment matrix).
|
||||
* - Pass-through actors (`super_admin`) may update anyone.
|
||||
* - `admin` may edit the basic info of any user but cannot change the
|
||||
* email of protected users (`super_admin`). User type changes are
|
||||
* still governed by the assignment matrix.
|
||||
* - Everyone else can only update their own record.
|
||||
*/
|
||||
export default function canUpdateUser({
|
||||
actor_user_types,
|
||||
actor_user_id,
|
||||
target_user_id,
|
||||
target_user_types,
|
||||
update_fields,
|
||||
}: Params): boolean {
|
||||
if (
|
||||
target_user_id &&
|
||||
actor_user_id &&
|
||||
String(target_user_id) === String(actor_user_id)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isPassThroughUser({ user_types: actor_user_types })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const can_edit_basic_info = checkUserAccess({
|
||||
user_types: actor_user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!can_edit_basic_info.success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const touches_contacts = update_fields?.some((field) =>
|
||||
ContactUpdateFields.includes(field),
|
||||
);
|
||||
|
||||
if (!touches_contacts) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
target_user_types?.some((ut) =>
|
||||
ContactProtectedUserTypes.includes(ut.user_type as UserType),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USER_TYPES } from "@/db/types/db";
|
||||
import type { UserType } from "@/src/types";
|
||||
import userTypeAssignment from "./user-type-assignment";
|
||||
|
||||
type Params = {
|
||||
actor_user_types?: BUN_SQLITE_BSQLITE_USER_TYPES[] | null;
|
||||
user_types: UserType[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters a list of user types down to the ones the actor may assign
|
||||
* (grant or remove), per the assignment matrix.
|
||||
*/
|
||||
export default function filterAssignableUserTypes({
|
||||
actor_user_types,
|
||||
user_types,
|
||||
}: Params): UserType[] {
|
||||
return user_types.filter((target_user_type) =>
|
||||
userTypeAssignment({
|
||||
actor_user_types,
|
||||
target_user_type,
|
||||
action: "assign",
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
canCreateUser,
|
||||
canListUsers,
|
||||
canManageUserTypes,
|
||||
canRevokeUser,
|
||||
canUpdateUser,
|
||||
filterAssignableUserTypes,
|
||||
isPassThroughUser,
|
||||
} from "./";
|
||||
import {
|
||||
ALL_USER_TYPES,
|
||||
makeUserTypes,
|
||||
} from "./user-type-assignment.test-fixtures";
|
||||
|
||||
describe("isPassThroughUser", () => {
|
||||
test("super_admin is pass-through", () => {
|
||||
expect(isPassThroughUser({ user_types: makeUserTypes("super_admin") })).toBe(true);
|
||||
});
|
||||
|
||||
test("regular types are not pass-through", () => {
|
||||
for (const type of ["admin", "president", "secretary"]) {
|
||||
expect(
|
||||
isPassThroughUser({ user_types: makeUserTypes(type) }),
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("canListUsers / canCreateUser / canManageUserTypes", () => {
|
||||
test("pass-through always passes", () => {
|
||||
for (const fn of [canListUsers, canCreateUser, canManageUserTypes]) {
|
||||
expect(
|
||||
fn({ user_types: makeUserTypes("super_admin") }),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("admin passes", () => {
|
||||
expect(canListUsers({ user_types: makeUserTypes("admin") })).toBe(true);
|
||||
expect(canCreateUser({ user_types: makeUserTypes("admin") })).toBe(true);
|
||||
expect(
|
||||
canManageUserTypes({ user_types: makeUserTypes("admin") }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("non-admin types fail", () => {
|
||||
for (const type of ["auditor", "president", "secretary", "read_only"]) {
|
||||
expect(
|
||||
canListUsers({ user_types: makeUserTypes(type) }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
canCreateUser({ user_types: makeUserTypes(type) }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
canManageUserTypes({ user_types: makeUserTypes(type) }),
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("canUpdateUser", () => {
|
||||
test("actors may update their own record", () => {
|
||||
expect(
|
||||
canUpdateUser({
|
||||
actor_user_types: makeUserTypes("auditor"),
|
||||
actor_user_id: 5,
|
||||
target_user_id: 5,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("pass-through may update anyone including contacts", () => {
|
||||
expect(
|
||||
canUpdateUser({
|
||||
actor_user_types: makeUserTypes("super_admin"),
|
||||
actor_user_id: 1,
|
||||
target_user_id: 2,
|
||||
target_user_types: makeUserTypes("super_admin"),
|
||||
update_fields: ["email"],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("admin may edit basic info of others", () => {
|
||||
expect(
|
||||
canUpdateUser({
|
||||
actor_user_types: makeUserTypes("admin"),
|
||||
actor_user_id: 1,
|
||||
target_user_id: 2,
|
||||
update_fields: ["first_name"],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("admin cannot change contacts of protected users", () => {
|
||||
expect(
|
||||
canUpdateUser({
|
||||
actor_user_types: makeUserTypes("admin"),
|
||||
actor_user_id: 1,
|
||||
target_user_id: 2,
|
||||
target_user_types: makeUserTypes("super_admin"),
|
||||
update_fields: ["email"],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("non-admin actors cannot update others", () => {
|
||||
expect(
|
||||
canUpdateUser({
|
||||
actor_user_types: makeUserTypes("auditor"),
|
||||
actor_user_id: 1,
|
||||
target_user_id: 2,
|
||||
update_fields: ["first_name"],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canRevokeUser", () => {
|
||||
test("super_admin targets can never be revoked", () => {
|
||||
expect(
|
||||
canRevokeUser({
|
||||
user_types: makeUserTypes("super_admin"),
|
||||
target_user_types: makeUserTypes("super_admin"),
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("pass-through may revoke anyone else", () => {
|
||||
expect(
|
||||
canRevokeUser({
|
||||
user_types: makeUserTypes("super_admin"),
|
||||
target_user_types: makeUserTypes("admin"),
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("admin may revoke revocable targets", () => {
|
||||
expect(
|
||||
canRevokeUser({
|
||||
user_types: makeUserTypes("admin"),
|
||||
target_user_types: makeUserTypes("read_only"),
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("admin cannot revoke protected targets", () => {
|
||||
expect(
|
||||
canRevokeUser({
|
||||
user_types: makeUserTypes("admin"),
|
||||
target_user_types: makeUserTypes("admin"),
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
canRevokeUser({
|
||||
user_types: makeUserTypes("admin"),
|
||||
target_user_types: makeUserTypes("president"),
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterAssignableUserTypes", () => {
|
||||
test("super_admin can assign everything", () => {
|
||||
const assignable = filterAssignableUserTypes({
|
||||
actor_user_types: makeUserTypes("super_admin"),
|
||||
user_types: ALL_USER_TYPES,
|
||||
});
|
||||
expect(assignable).toHaveLength(ALL_USER_TYPES.length);
|
||||
});
|
||||
|
||||
test("admin sees only assignable types", () => {
|
||||
const assignable = filterAssignableUserTypes({
|
||||
actor_user_types: makeUserTypes("admin"),
|
||||
user_types: ALL_USER_TYPES,
|
||||
});
|
||||
expect(assignable).toContain("auditor");
|
||||
expect(assignable).toContain("read_only");
|
||||
expect(assignable).not.toContain("admin");
|
||||
expect(assignable).not.toContain("super_admin");
|
||||
expect(assignable).not.toContain("president");
|
||||
expect(assignable).not.toContain("secretary");
|
||||
});
|
||||
|
||||
test("secretary can assign nothing", () => {
|
||||
const assignable = filterAssignableUserTypes({
|
||||
actor_user_types: makeUserTypes("secretary"),
|
||||
user_types: ALL_USER_TYPES,
|
||||
});
|
||||
expect(assignable).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
export { default as userTypeAssignment } from "./user-type-assignment";
|
||||
export { default as canCreateUser } from "./can-create-user";
|
||||
export { default as canListUsers } from "./can-list-users";
|
||||
export { default as canManageUserTypes } from "./can-manage-user-types";
|
||||
export { default as canRevokeUser } from "./can-revoke-user";
|
||||
export { default as canUpdateUser } from "./can-update-user";
|
||||
export { default as filterAssignableUserTypes } from "./filter-assignable-user-types";
|
||||
export { default as isPassThroughUser } from "./is-pass-through-user";
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USER_TYPES } from "@/db/types/db";
|
||||
|
||||
type Params = {
|
||||
user_types?: BUN_SQLITE_BSQLITE_USER_TYPES[] | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pass-through actors that bypass the assignment matrix: `super_admin`.
|
||||
*/
|
||||
export default function isPassThroughUser({ user_types }: Params): boolean {
|
||||
return Boolean(
|
||||
user_types?.find((ut) => ut.user_type === "super_admin"),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USER_TYPES } from "@/db/types/db";
|
||||
|
||||
type Params = {
|
||||
user_types?: BUN_SQLITE_BSQLITE_USER_TYPES[] | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether this is a `read_only` user
|
||||
*/
|
||||
export default function isReadOnlyUser({ user_types }: Params): boolean {
|
||||
return Boolean(user_types?.find((ty) => ty.user_type == "read_only"));
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type {
|
||||
UserType,
|
||||
UserTypeAssignmentMatrixType,
|
||||
} from "@/src/types";
|
||||
|
||||
/**
|
||||
* Synthetic types (`orphan`, `board_member`) exercise the matrix interpreter
|
||||
* independently of the preset's real `UserTypes`.
|
||||
*/
|
||||
export const ALL_USER_TYPES = [
|
||||
"super_admin",
|
||||
"admin",
|
||||
"auditor",
|
||||
"president",
|
||||
"vice_president",
|
||||
"secretary",
|
||||
"orphan",
|
||||
"revoked",
|
||||
"read_only",
|
||||
"board_member",
|
||||
] as UserType[];
|
||||
|
||||
/**
|
||||
* Matrix keyed by any type name, cast to the real matrix type for the
|
||||
* interpreter.
|
||||
*/
|
||||
type FixtureMatrix = Record<string, any>;
|
||||
|
||||
type UserTypesInput = string[];
|
||||
|
||||
export function makeUserTypes(...types: UserTypesInput): { user_type: string }[] {
|
||||
return types.map((user_type) => ({ user_type }));
|
||||
}
|
||||
|
||||
export function makeMatrix(
|
||||
overrides?: FixtureMatrix,
|
||||
): UserTypeAssignmentMatrixType {
|
||||
const base: FixtureMatrix = {
|
||||
super_admin: {},
|
||||
admin: { allowed_actors: [] },
|
||||
president: { allowed_actors: [] },
|
||||
vice_president: { allowed_actors: [] },
|
||||
secretary: { allowed_actors: [] },
|
||||
board_member: { allowed_actors: ["admin"] },
|
||||
auditor: { allowed_actors: ["admin"] },
|
||||
orphan: { allowed_actors: ["admin"] },
|
||||
read_only: {
|
||||
allowed_actors: {
|
||||
admin: { excluded_targets: ["super_admin"] },
|
||||
},
|
||||
},
|
||||
revoked: {
|
||||
allowed_actors: {
|
||||
admin: {
|
||||
allowed_targets: ["board_member", "auditor", "orphan"],
|
||||
excluded_targets: [
|
||||
"super_admin",
|
||||
"admin",
|
||||
"president",
|
||||
"vice_president",
|
||||
"secretary",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...base,
|
||||
...overrides,
|
||||
} as UserTypeAssignmentMatrixType;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { UserType } from "@/src/types";
|
||||
import userTypeAssignment from "./user-type-assignment";
|
||||
import {
|
||||
ALL_USER_TYPES,
|
||||
makeMatrix,
|
||||
makeUserTypes,
|
||||
} from "./user-type-assignment.test-fixtures";
|
||||
|
||||
const ASSIGNMENT_MATRIX = makeMatrix();
|
||||
const DELETION_MATRIX = makeMatrix();
|
||||
|
||||
function assign(params: {
|
||||
actor: string[];
|
||||
target_type: string;
|
||||
target?: string[];
|
||||
action?: "assign" | "delete";
|
||||
}) {
|
||||
return userTypeAssignment({
|
||||
actor_user_types: makeUserTypes(...params.actor),
|
||||
target_user_type: params.target_type as UserType,
|
||||
target_user_types: params.target as UserType[],
|
||||
action: params.action || "assign",
|
||||
assignment_matrix: ASSIGNMENT_MATRIX,
|
||||
deletion_matrix: DELETION_MATRIX,
|
||||
});
|
||||
}
|
||||
|
||||
describe("userTypeAssignment", () => {
|
||||
test("pass-through (super_admin) can assign anything", () => {
|
||||
for (const target of ALL_USER_TYPES) {
|
||||
expect(
|
||||
assign({ actor: ["super_admin"], target_type: target }),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("pass-through (super_admin) can delete anything", () => {
|
||||
for (const target of ALL_USER_TYPES) {
|
||||
expect(
|
||||
assign({
|
||||
actor: ["super_admin"],
|
||||
target_type: target,
|
||||
action: "delete",
|
||||
}),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("no actors allowed for super_admin", () => {
|
||||
expect(
|
||||
assign({ actor: ["admin"], target_type: "super_admin" }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
assign({
|
||||
actor: ["admin"],
|
||||
target_type: "super_admin",
|
||||
action: "delete",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("empty allowed_actors means pass-through only", () => {
|
||||
for (const target of ["admin", "president", "vice_president", "secretary"]) {
|
||||
expect(assign({ actor: ["admin"], target_type: target })).toBe(
|
||||
false,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("array allowed_actors matches any actor", () => {
|
||||
expect(assign({ actor: ["admin"], target_type: "board_member" })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(assign({ actor: ["auditor"], target_type: "board_member" })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(assign({ actor: ["admin"], target_type: "auditor" })).toBe(true);
|
||||
});
|
||||
|
||||
test("per-actor rules respect excluded targets", () => {
|
||||
expect(
|
||||
assign({
|
||||
actor: ["admin"],
|
||||
target_type: "read_only",
|
||||
target: ["admin"],
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
assign({
|
||||
actor: ["admin"],
|
||||
target_type: "read_only",
|
||||
target: ["super_admin"],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("per-actor rules respect allowed targets", () => {
|
||||
expect(
|
||||
assign({
|
||||
actor: ["admin"],
|
||||
target_type: "revoked",
|
||||
target: ["board_member"],
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
assign({
|
||||
actor: ["admin"],
|
||||
target_type: "revoked",
|
||||
target: ["admin"],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("missing target user types fail allowed_targets checks", () => {
|
||||
expect(
|
||||
assign({
|
||||
actor: ["admin"],
|
||||
target_type: "revoked",
|
||||
target: [],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("same_as clones rules of another actor", () => {
|
||||
const matrix = makeMatrix({
|
||||
read_only: {
|
||||
allowed_actors: {
|
||||
admin: { excluded_targets: ["super_admin"] },
|
||||
auditor: { same_as: "admin" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
userTypeAssignment({
|
||||
actor_user_types: makeUserTypes("auditor"),
|
||||
target_user_type: "read_only",
|
||||
target_user_types: ["admin"] as UserType[],
|
||||
action: "assign",
|
||||
assignment_matrix: matrix,
|
||||
deletion_matrix: matrix,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
userTypeAssignment({
|
||||
actor_user_types: makeUserTypes("auditor"),
|
||||
target_user_type: "read_only",
|
||||
target_user_types: ["super_admin"] as UserType[],
|
||||
action: "assign",
|
||||
assignment_matrix: matrix,
|
||||
deletion_matrix: matrix,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("all flag grants unconditional access", () => {
|
||||
const matrix = makeMatrix({
|
||||
board_member: {
|
||||
allowed_actors: {
|
||||
admin: { all: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
userTypeAssignment({
|
||||
actor_user_types: makeUserTypes("admin"),
|
||||
target_user_type: "board_member" as UserType,
|
||||
target_user_types: ["super_admin"] as UserType[],
|
||||
action: "assign",
|
||||
assignment_matrix: matrix,
|
||||
deletion_matrix: matrix,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
describe("unknown user types", () => {
|
||||
test("types missing from the matrix are denied instead of throwing", () => {
|
||||
expect(
|
||||
userTypeAssignment({
|
||||
actor_user_types: makeUserTypes("admin"),
|
||||
target_user_type: "not_in_matrix" as UserType,
|
||||
action: "assign",
|
||||
assignment_matrix: makeMatrix(),
|
||||
deletion_matrix: makeMatrix(),
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import type {
|
||||
UserAssignmentFnParams,
|
||||
UserTypeAssignmentRules,
|
||||
} from "@/src/types";
|
||||
import {
|
||||
UserTypeAssignmentMatrix,
|
||||
UserTypeDeletionMatrix,
|
||||
} from "@/src/dict/user-types-dict";
|
||||
import isPassThroughUser from "./is-pass-through-user";
|
||||
|
||||
/**
|
||||
* Whether the actor may assign or remove `target_user_type` on a user.
|
||||
* Governed by `UserTypeAssignmentMatrix` and `UserTypeDeletionMatrix` and
|
||||
* applied symmetrically to granting and removing. Pass-through actors
|
||||
* (`super_admin`) can always assign.
|
||||
*/
|
||||
export default function userTypeAssignment({
|
||||
actor_user_types,
|
||||
target_user_type,
|
||||
target_user_types,
|
||||
action,
|
||||
assignment_matrix = UserTypeAssignmentMatrix,
|
||||
deletion_matrix = UserTypeDeletionMatrix,
|
||||
debug,
|
||||
}: UserAssignmentFnParams): boolean {
|
||||
if (isPassThroughUser({ user_types: actor_user_types })) {
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[RBAC] Pass-through actor detected for [${target_user_type}], access granted.`,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const matrix = action == "assign" ? assignment_matrix : deletion_matrix;
|
||||
|
||||
const assignable_by = matrix[target_user_type];
|
||||
|
||||
if (!assignable_by?.allowed_actors) {
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[RBAC] No allowed actors for [${target_user_type}] in the ${action} matrix.`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const is_target_user_type_excluded =
|
||||
target_user_types && assignable_by.excluded_targets
|
||||
? target_user_types.find((tut) =>
|
||||
assignable_by.excluded_targets?.find((ex) => ex == tut),
|
||||
)
|
||||
: false;
|
||||
|
||||
if (is_target_user_type_excluded) {
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[RBAC] Target user type [${target_user_types?.join(",")}] excluded for [${target_user_type}].`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const actor_type_set = new Set<string>(
|
||||
actor_user_types
|
||||
?.map((ut) => ut.user_type)
|
||||
.filter((ut): ut is string => Boolean(ut)),
|
||||
);
|
||||
|
||||
if (Array.isArray(assignable_by.allowed_actors)) {
|
||||
if (!assignable_by.allowed_actors.length) {
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[RBAC] Actor not allowed for [${target_user_type}], allowed actors list is empty.`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return assignable_by.allowed_actors.some((actor_type) =>
|
||||
actor_type_set.has(actor_type),
|
||||
);
|
||||
}
|
||||
|
||||
const allowed_actors_matrix = assignable_by.allowed_actors as Record<
|
||||
string,
|
||||
UserTypeAssignmentRules | undefined
|
||||
>;
|
||||
|
||||
if (!Object.keys(allowed_actors_matrix).length) {
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[RBAC] No allowed actors matrix for [${target_user_type}].`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return Array.from(actor_type_set).some((actor_type) => {
|
||||
let actor_rules = allowed_actors_matrix[actor_type];
|
||||
|
||||
if (
|
||||
actor_rules?.same_as &&
|
||||
allowed_actors_matrix[actor_rules.same_as]
|
||||
) {
|
||||
actor_rules = {
|
||||
...actor_rules,
|
||||
...allowed_actors_matrix[actor_rules.same_as],
|
||||
};
|
||||
}
|
||||
|
||||
if (actor_rules?.all) {
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[RBAC] Actor [${actor_type}] has all access for [${target_user_type}].`,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!actor_rules?.allowed_targets && !actor_rules?.excluded_targets) {
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[RBAC] No actor rules for [${actor_type}] on [${target_user_type}].`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
actor_rules.allowed_targets &&
|
||||
!actor_rules.allowed_targets.find((t) =>
|
||||
target_user_types?.includes(t),
|
||||
)
|
||||
) {
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[RBAC] Target user types [${target_user_types?.join(",")}] not included for [${actor_type}] -> [${target_user_type}].`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
actor_rules.excluded_targets &&
|
||||
actor_rules.excluded_targets.find((t) =>
|
||||
target_user_types?.includes(t),
|
||||
)
|
||||
) {
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[RBAC] Target user types [${target_user_types?.join(",")}] excluded for [${actor_type}] -> [${target_user_type}].`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[RBAC] Actor [${actor_type}] granted [${action}] of [${target_user_type}].`,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user