-
-
Notifications
You must be signed in to change notification settings - Fork 288
Feature: page access groups with RBAC-based visibility control #727
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import type { Knex } from "knex"; | ||
|
|
||
| export async function up(knex: Knex): Promise<void> { | ||
| // Access groups define visibility scopes for pages. | ||
| // "public" = visible to everyone without login. | ||
| // "admin" = grants access to all pages (assigned to admin role). | ||
| if (!(await knex.schema.hasTable("access_groups"))) { | ||
| await knex.schema.createTable("access_groups", (table) => { | ||
| table.string("id", 100).primary(); | ||
| table.text("group_name").notNullable(); | ||
| table.text("description"); | ||
| table.integer("is_system").notNullable().defaultTo(0); | ||
| table.timestamp("created_at").defaultTo(knex.fn.now()); | ||
| table.timestamp("updated_at").defaultTo(knex.fn.now()); | ||
| }); | ||
| } | ||
|
|
||
| // Junction: which access groups are assigned to which page | ||
| if (!(await knex.schema.hasTable("page_access_groups"))) { | ||
| await knex.schema.createTable("page_access_groups", (table) => { | ||
| table.integer("page_id").unsigned().notNullable() | ||
| .references("id").inTable("pages").onDelete("CASCADE"); | ||
| table.string("access_group_id", 100).notNullable() | ||
| .references("id").inTable("access_groups").onDelete("CASCADE"); | ||
| table.timestamp("created_at").defaultTo(knex.fn.now()); | ||
|
|
||
| table.primary(["page_id", "access_group_id"]); | ||
| }); | ||
| } | ||
|
|
||
| // Junction: which access groups a role can see | ||
| if (!(await knex.schema.hasTable("role_access_groups"))) { | ||
| await knex.schema.createTable("role_access_groups", (table) => { | ||
| table.string("role_id", 100).notNullable() | ||
| .references("id").inTable("roles").onDelete("CASCADE"); | ||
| table.string("access_group_id", 100).notNullable() | ||
| .references("id").inTable("access_groups").onDelete("CASCADE"); | ||
| table.timestamp("created_at").defaultTo(knex.fn.now()); | ||
|
|
||
| table.primary(["role_id", "access_group_id"]); | ||
| }); | ||
| } | ||
|
|
||
| // Seed the "public" system group | ||
| const publicExists = await knex("access_groups").where("id", "public").first(); | ||
| if (!publicExists) { | ||
| await knex("access_groups").insert({ | ||
| id: "public", | ||
| group_name: "Public", | ||
| description: "Pages with this group are visible to everyone without login", | ||
| is_system: 1, | ||
| created_at: knex.fn.now(), | ||
| updated_at: knex.fn.now(), | ||
| }); | ||
| } | ||
|
|
||
| // Seed the "admin" system group | ||
| const adminExists = await knex("access_groups").where("id", "admin").first(); | ||
| if (!adminExists) { | ||
| await knex("access_groups").insert({ | ||
| id: "admin", | ||
| group_name: "Admin", | ||
| description: "Grants access to all pages regardless of their groups", | ||
| is_system: 1, | ||
| created_at: knex.fn.now(), | ||
| updated_at: knex.fn.now(), | ||
| }); | ||
| } | ||
|
|
||
| // Assign "public" to all existing pages (backward compatibility) | ||
| const allPages = await knex("pages").select("id"); | ||
| for (const page of allPages) { | ||
| const exists = await knex("page_access_groups") | ||
| .where({ page_id: page.id, access_group_id: "public" }) | ||
| .first(); | ||
| if (!exists) { | ||
| await knex("page_access_groups").insert({ | ||
| page_id: page.id, | ||
| access_group_id: "public", | ||
| created_at: knex.fn.now(), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // Assign "admin" group to the admin role | ||
| const adminRoleExists = await knex("roles").where("id", "admin").first(); | ||
| if (adminRoleExists) { | ||
| const alreadyAssigned = await knex("role_access_groups") | ||
| .where({ role_id: "admin", access_group_id: "admin" }) | ||
| .first(); | ||
| if (!alreadyAssigned) { | ||
| await knex("role_access_groups").insert({ | ||
| role_id: "admin", | ||
| access_group_id: "admin", | ||
| created_at: knex.fn.now(), | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export async function down(knex: Knex): Promise<void> { | ||
| await knex.schema.dropTableIfExists("role_access_groups"); | ||
| await knex.schema.dropTableIfExists("page_access_groups"); | ||
| await knex.schema.dropTableIfExists("access_groups"); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,18 +1,27 @@ | ||||||||||||||||||||||||||||||||||||||||||
| import db from "../db/db.js"; | ||||||||||||||||||||||||||||||||||||||||||
| import type { PageRecord, PageRecordInsert, PageMonitorRecord, PageMonitorRecordInsert } from "../types/db.js"; | ||||||||||||||||||||||||||||||||||||||||||
| import type { | ||||||||||||||||||||||||||||||||||||||||||
| PageRecord, | ||||||||||||||||||||||||||||||||||||||||||
| PageRecordInsert, | ||||||||||||||||||||||||||||||||||||||||||
| PageMonitorRecord, | ||||||||||||||||||||||||||||||||||||||||||
| PageMonitorRecordInsert, | ||||||||||||||||||||||||||||||||||||||||||
| UserRecordPublic, | ||||||||||||||||||||||||||||||||||||||||||
| } from "../types/db.js"; | ||||||||||||||||||||||||||||||||||||||||||
| import { GetSiteDataByKey } from "./siteDataController.js"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // ============ Page CRUD Operations ============ | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Create a new page | ||||||||||||||||||||||||||||||||||||||||||
| * Create a new page. | ||||||||||||||||||||||||||||||||||||||||||
| * When the site setting "autoPublicPages" is true (default), | ||||||||||||||||||||||||||||||||||||||||||
| * newly created pages automatically get the "public" access group. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function CreatePage(data: PageRecordInsert): Promise<PageRecord> { | ||||||||||||||||||||||||||||||||||||||||||
| // Validate required fields | ||||||||||||||||||||||||||||||||||||||||||
| if (data.page_path === undefined || data.page_path === null || !data.page_title || !data.page_header) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error("page_path, page_title, and page_header are required"); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Make page_path URL-friendly: lowercase, replace spaces with hyphens, remove special chars (including leading slashes) | ||||||||||||||||||||||||||||||||||||||||||
| // Make page_path URL-friendly | ||||||||||||||||||||||||||||||||||||||||||
| data.page_path = data.page_path | ||||||||||||||||||||||||||||||||||||||||||
| .toLowerCase() | ||||||||||||||||||||||||||||||||||||||||||
| .trim() | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -25,7 +34,15 @@ export async function CreatePage(data: PageRecordInsert): Promise<PageRecord> { | |||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Page with path "${data.page_path}" already exists`); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return await db.createPage(data); | ||||||||||||||||||||||||||||||||||||||||||
| const newPage = await db.createPage(data); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Auto-assign "public" group to new pages (unless disabled in settings) | ||||||||||||||||||||||||||||||||||||||||||
| const autoPublic = await GetSiteDataByKey("autoPublicPages"); | ||||||||||||||||||||||||||||||||||||||||||
| if (autoPublic !== false && autoPublic !== "false") { | ||||||||||||||||||||||||||||||||||||||||||
| await db.setPageAccessGroups(newPage.id, ["public"]); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Keep page creation and default access assignment atomic. If Localized compensation option const newPage = await db.createPage(data);
- // Auto-assign "public" group to new pages (unless disabled in settings)
- const autoPublic = await GetSiteDataByKey("autoPublicPages");
- if (autoPublic !== false && autoPublic !== "false") {
- await db.setPageAccessGroups(newPage.id, ["public"]);
+ try {
+ // Auto-assign "public" group to new pages (unless disabled in settings)
+ const autoPublic = await GetSiteDataByKey("autoPublicPages");
+ if (autoPublic !== false && autoPublic !== "false") {
+ await db.setPageAccessGroups(newPage.id, ["public"]);
+ }
+ } catch (error) {
+ await db.deletePage(newPage.id);
+ throw error;
}
return newPage;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return newPage; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -228,3 +245,176 @@ export async function GetPageByPathWithMonitors( | |||||||||||||||||||||||||||||||||||||||||
| const monitors = await db.getPageMonitorsExcludeHidden(page.id); | ||||||||||||||||||||||||||||||||||||||||||
| return { page, monitors }; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // ============ Access Group Operations ============ | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Reserved group ID: role with "admin" can see all pages | ||||||||||||||||||||||||||||||||||||||||||
| const ADMIN_GROUP = "admin"; | ||||||||||||||||||||||||||||||||||||||||||
| // Reserved group ID: pages with "public" are visible without login | ||||||||||||||||||||||||||||||||||||||||||
| const PUBLIC_GROUP = "public"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Check if a user can access a specific page. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * Rules (evaluated top to bottom, first match wins): | ||||||||||||||||||||||||||||||||||||||||||
| * 1. Page has "public" group → allow (no login needed) | ||||||||||||||||||||||||||||||||||||||||||
| * 2. No login → redirect to sign-in | ||||||||||||||||||||||||||||||||||||||||||
| * 3. User's roles include the "admin" group → allow (admin sees everything) | ||||||||||||||||||||||||||||||||||||||||||
| * 4. Page has no access groups → denied (unconfigured pages are hidden) | ||||||||||||||||||||||||||||||||||||||||||
| * 5. Any overlap between page groups and role groups → allow | ||||||||||||||||||||||||||||||||||||||||||
| * 6. No overlap → denied | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * Returns: "allow" | "login_required" | "denied" | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function CheckPageAccess( | ||||||||||||||||||||||||||||||||||||||||||
| page_id: number, | ||||||||||||||||||||||||||||||||||||||||||
| user: UserRecordPublic | null, | ||||||||||||||||||||||||||||||||||||||||||
| ): Promise<"allow" | "login_required" | "denied"> { | ||||||||||||||||||||||||||||||||||||||||||
| const pageGroups = await db.getAccessGroupsForPage(page_id); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Rule 1: public pages are visible to everyone | ||||||||||||||||||||||||||||||||||||||||||
| if (pageGroups.includes(PUBLIC_GROUP)) { | ||||||||||||||||||||||||||||||||||||||||||
| return "allow"; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Rule 2: non-public pages require login | ||||||||||||||||||||||||||||||||||||||||||
| if (!user) { | ||||||||||||||||||||||||||||||||||||||||||
| return "login_required"; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Rule 3: admin group grants access to all pages | ||||||||||||||||||||||||||||||||||||||||||
| const roleGroups = await db.getAccessGroupsForRoles(user.role_ids); | ||||||||||||||||||||||||||||||||||||||||||
| if (roleGroups.includes(ADMIN_GROUP)) { | ||||||||||||||||||||||||||||||||||||||||||
| return "allow"; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Rule 4: pages with no groups are hidden (except for admin) | ||||||||||||||||||||||||||||||||||||||||||
| if (pageGroups.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||
| return "denied"; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Rule 5+6: check for overlap between page and role groups | ||||||||||||||||||||||||||||||||||||||||||
| const hasAccess = pageGroups.some((pg) => roleGroups.includes(pg)); | ||||||||||||||||||||||||||||||||||||||||||
| return hasAccess ? "allow" : "denied"; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Get all pages that a user is allowed to see. | ||||||||||||||||||||||||||||||||||||||||||
| * Used by the page switcher to filter the dropdown. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function GetAccessiblePages( | ||||||||||||||||||||||||||||||||||||||||||
| user: UserRecordPublic | null, | ||||||||||||||||||||||||||||||||||||||||||
| ): Promise<PageRecord[]> { | ||||||||||||||||||||||||||||||||||||||||||
| const allPages = await db.getAllPages(); | ||||||||||||||||||||||||||||||||||||||||||
| if (allPages.length === 0) return []; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Load access groups for all pages in one query | ||||||||||||||||||||||||||||||||||||||||||
| const pageIds = allPages.map((p) => p.id); | ||||||||||||||||||||||||||||||||||||||||||
| const pageGroupsMap = await db.getAccessGroupsForPages(pageIds); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Load user's role groups (if logged in) | ||||||||||||||||||||||||||||||||||||||||||
| let roleGroups: string[] = []; | ||||||||||||||||||||||||||||||||||||||||||
| let isAdmin = false; | ||||||||||||||||||||||||||||||||||||||||||
| if (user) { | ||||||||||||||||||||||||||||||||||||||||||
| roleGroups = await db.getAccessGroupsForRoles(user.role_ids); | ||||||||||||||||||||||||||||||||||||||||||
| isAdmin = roleGroups.includes(ADMIN_GROUP); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return allPages.filter((page) => { | ||||||||||||||||||||||||||||||||||||||||||
| const groups = pageGroupsMap.get(page.id) || []; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Public pages are visible to everyone | ||||||||||||||||||||||||||||||||||||||||||
| if (groups.includes(PUBLIC_GROUP)) return true; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Non-public pages require login | ||||||||||||||||||||||||||||||||||||||||||
| if (!user) return false; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Admin sees everything | ||||||||||||||||||||||||||||||||||||||||||
| if (isAdmin) return true; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Pages with no groups are hidden | ||||||||||||||||||||||||||||||||||||||||||
| if (groups.length === 0) return false; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Check if role groups overlap with page groups | ||||||||||||||||||||||||||||||||||||||||||
| return groups.some((g) => roleGroups.includes(g)); | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // ============ Access Group Admin Operations ============ | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Get all access groups (for admin UI dropdowns) | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function GetAllAccessGroups() { | ||||||||||||||||||||||||||||||||||||||||||
| return await db.getAllAccessGroups(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Create a new access group | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function CreateAccessGroup(data: { id: string; group_name: string; description?: string }) { | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+351
to
+355
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The function blocks creation of a group named |
||||||||||||||||||||||||||||||||||||||||||
| if (!data.id || !data.group_name) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error("id and group_name are required"); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Normalize ID: lowercase, hyphens, no special chars | ||||||||||||||||||||||||||||||||||||||||||
| data.id = data.id.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9_-]/g, ""); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (data.id === "public") { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Cannot create a group with the reserved ID 'public'"); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+360
to
+365
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Revalidate normalized IDs and reserve An ID like Suggested fix // Normalize ID: lowercase, hyphens, no special chars
data.id = data.id.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9_-]/g, "");
- if (data.id === "public") {
- throw new Error("Cannot create a group with the reserved ID 'public'");
+ if (!data.id) {
+ throw new Error("id must contain at least one URL-safe character");
+ }
+
+ if (data.id === PUBLIC_GROUP || data.id === ADMIN_GROUP) {
+ throw new Error(`Cannot create a group with the reserved ID '${data.id}'`);
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const existing = (await db.getAllAccessGroups()).find((g) => g.id === data.id); | ||||||||||||||||||||||||||||||||||||||||||
| if (existing) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Access group '${data.id}' already exists`); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| await db.createAccessGroup(data); | ||||||||||||||||||||||||||||||||||||||||||
| return { success: true, id: data.id }; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Delete an access group. System groups (public, admin) cannot be deleted. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function DeleteAccessGroup(id: string) { | ||||||||||||||||||||||||||||||||||||||||||
| const allGroups = await db.getAllAccessGroups(); | ||||||||||||||||||||||||||||||||||||||||||
| const group = allGroups.find((g) => g.id === id); | ||||||||||||||||||||||||||||||||||||||||||
| if (!group) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Access group '${id}' not found`); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| if ((group as any).is_system === 1) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Cannot delete system group '${id}'`); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| await db.deleteAccessGroup(id); | ||||||||||||||||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Get access groups assigned to a page | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function GetPageAccessGroups(page_id: number): Promise<string[]> { | ||||||||||||||||||||||||||||||||||||||||||
| return await db.getAccessGroupsForPage(page_id); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Set access groups for a page (replaces all existing assignments) | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function SetPageAccessGroups(page_id: number, group_ids: string[]) { | ||||||||||||||||||||||||||||||||||||||||||
| await db.setPageAccessGroups(page_id, group_ids); | ||||||||||||||||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Get access groups assigned to a role | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function GetRoleAccessGroups(role_id: string): Promise<string[]> { | ||||||||||||||||||||||||||||||||||||||||||
| return await db.getAccessGroupsForRole(role_id); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Set access groups for a role (replaces all existing assignments) | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export async function SetRoleAccessGroups(role_id: string, group_ids: string[]) { | ||||||||||||||||||||||||||||||||||||||||||
| await db.setRoleAccessGroups(role_id, group_ids); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+402
to
+418
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Validate assignment group IDs before writing. The manage API passes 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Require page-write permission when deleting access groups.
deleteAccessGroupcan cascade-removepage_access_groups, soroles.writealone lets a role manager change page visibility. Add apages.writecheck for this action, or require bothroles.writeandpages.writein the dispatcher.🤖 Prompt for AI Agents