Skip to content

Feat/4311 category permission enforcement - #4553

Merged
thorsten merged 14 commits into
mainfrom
feat/4311-category-permission-enforcement
Aug 10, 2026
Merged

Feat/4311 category permission enforcement#4553
thorsten merged 14 commits into
mainfrom
feat/4311-category-permission-enforcement

Conversation

@thorsten

@thorsten thorsten commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added category-scoped permissions for managing FAQs and categories.
    • Restricted access now applies to creating, editing, translating, reordering, activating, marking content as sticky, and deleting.
    • Administration views show only categories available for each action.
    • Uncategorized content follows documented permission rules.
    • Basic permission mode remains unrestricted by category.
    • Unauthorized category actions now return a forbidden response.
  • Documentation

    • Added guidance covering category restrictions, permission exceptions, imports, translations, forbidden responses, and uncategorized FAQs.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds category-scoped permission APIs for basic and medium modes. Administration controllers enforce these permissions for category and FAQ operations. FAQ administration views filter category trees. Tests and documentation cover restricted, unrestricted, and uncategorized content.

Changes

Category-scoped administration permissions

Layer / File(s) Summary
Permission contracts and resolution
phpmyfaq/src/phpMyFAQ/Permission/..., tests/phpMyFAQ/Permission/..., tests/phpMyFAQ/Administration/..., tests/phpMyFAQ/Attachment/...
PermissionInterface adds category checks and allowed-category lookup. Basic mode returns unrestricted access. Medium mode resolves direct, group, restricted, and missing grants.
Shared validation and tree filtering
phpmyfaq/src/phpMyFAQ/Controller/AbstractController.php, phpmyfaq/src/phpMyFAQ/Category/..., tests/phpMyFAQ/Category/..., docs/administration.md
Controllers validate authentication, category IDs, and permissions. Category trees are filtered by allowed IDs. Category relation output receives a precise array-shape annotation. Documentation describes category restrictions and uncategorized FAQs.
Administration API enforcement
phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/..., tests/phpMyFAQ/Controller/Administration/Api/...
Category and FAQ API operations enforce category permissions for creation, editing, deletion, translation, listing, activation, and sticky changes. Tests cover restricted-category and orphaned-FAQ cases.
Administration workflow restrictions
phpmyfaq/src/phpMyFAQ/Controller/Administration/..., tests/phpMyFAQ/Controller/Administration/...
Category workflows validate targeted categories. FAQ add, edit, copy, translation, and answer views receive operation-specific filtered category trees.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdminController
  participant AbstractController
  participant MediumPermission
  participant CategoryTreeRestrictionFilter
  participant FAQStore

  AdminController->>AbstractController: validate category permission
  AbstractController->>MediumPermission: check user right for categories
  MediumPermission-->>AbstractController: return authorization result
  AdminController->>CategoryTreeRestrictionFilter: filter allowed category tree
  CategoryTreeRestrictionFilter-->>AdminController: return filtered tree
  AdminController->>FAQStore: perform authorized FAQ operation
  FAQStore-->>AdminController: return operation result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the pull request's main change: enforcing category permissions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/4311-category-permission-enforcement

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/CategoryController.php (1)

150-185: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject unauthorized categories before saving the submitted order.

updateOrder only checks $categoryId, while $this->categoryOrder->setCategoryTree($categoryTree) persists order rows for every node in the submitted tree. Exclude or reject tree nodes outside the user’s granted category rights before setting the tree and before updating the parent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/CategoryController.php`
around lines 150 - 185, Update updateOrder to validate every category node in
categoryTree against the current user’s CATEGORY_EDIT rights before calling
setCategoryTree or updateParentCategory. Reject unauthorized nodes or remove
them from the submitted tree, while preserving authorized nodes and ensuring
parent lookup and persistence use only the validated tree.
phpmyfaq/src/phpMyFAQ/Controller/Administration/CategoryController.php (1)

69-118: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restrict category overview/hierarchy tree data with category permissions.

index() passes categoryTree, categoryInfo, and categoryTranslations derived from categoryInfo to the template; getAllCategories() loads the full category catalog. hierarchy() passes category->getCategoryTree() after buildCategoryTree() uses withPermission: false. Use a permission-scoped category list and filter the returned tree by the user’s allowed category IDs, as FAQ admin controllers do.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@phpmyfaq/src/phpMyFAQ/Controller/Administration/CategoryController.php`
around lines 69 - 118, Restrict category data in both index() and hierarchy() to
the current user’s permitted category IDs. Replace the unrestricted category
loading around Category and buildCategoryTree() with the established
permission-scoped category list used by FAQ admin controllers, then filter
categoryTree and categoryInfo-derived categoryTranslations consistently before
rendering.
🧹 Nitpick comments (3)
phpmyfaq/src/phpMyFAQ/Permission/MediumPermission.php (1)

588-627: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated right-resolution logic into a private helper.

The block that resolves $right to a right ID (numeric check, getRightId() for a name, getRightId() for a PermissionType) is duplicated in hasPermission, hasPermissionForCategory, and now getAllowedCategoriesForRight. Extract a private method, for example resolveRightId(mixed $right): int, and call it from all three places. This reduces the risk that a future fix to right resolution is applied in only one location.

♻️ Proposed helper extraction
+    private function resolveRightId(mixed $right): int
+    {
+        if (!is_numeric($right) && is_string($right)) {
+            $right = $this->getRightId($right);
+        }
+
+        if ($right instanceof PermissionType) {
+            $right = $this->getRightId($right->value);
+        }
+
+        return (int) $right;
+    }
+
     #[\Override]
     public function getAllowedCategoriesForRight(int $userId, mixed $right): ?array
     {
         $currentUser = new CurrentUser($this->configuration);
         $currentUser->getUserById($userId);

         if ($currentUser->isSuperAdmin()) {
             return null;
         }

-        if (!is_numeric($right) && is_string($right)) {
-            $right = $this->getRightId($right);
-        }
-
-        if ($right instanceof PermissionType) {
-            $right = $this->getRightId($right->value);
-        }
-
-        $rightId = (int) $right;
+        $rightId = $this->resolveRightId($right);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@phpmyfaq/src/phpMyFAQ/Permission/MediumPermission.php` around lines 588 -
627, Extract the duplicated right-resolution logic into a private
resolveRightId(mixed $right): int helper, preserving the existing numeric,
string, and PermissionType handling. Replace the corresponding resolution blocks
in hasPermission, hasPermissionForCategory, and getAllowedCategoriesForRight
with calls to this helper, then use its returned integer right ID.
phpmyfaq/src/phpMyFAQ/Controller/Administration/FaqController.php (1)

100-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated "allowed categories + filter tree" pattern into a helper.

The same three-line pattern — call getAllowedCategoriesForRight() for a PermissionType, then pass the result and $category->getCategoryTree() to CategoryTreeRestrictionFilter::filter() — repeats at lines 148-152/167-170, 228-232/247-250, 362-366/394-397, 452-456/474-477, 532-536/554-557, and 623-627/645-648. Extract a private helper, for example getFilteredCategoryTree(Category $category, PermissionType $permissionType): array, and call it from all seven sites. This removes duplication and keeps future fixes (such as the type-mismatch fix on line 305 above) in one place.

As per path instructions for **/src/phpMyFAQ/**/*.php: "Prefer guard clauses and early returns over deeply nested conditionals; keep functions small and single-purpose."

♻️ Proposed helper extraction
+    private function getFilteredCategoryTree(Category $category, PermissionType $permissionType): array
+    {
+        $allowed = $this->currentUser->perm->getAllowedCategoriesForRight(
+            $this->currentUser->getUserId(),
+            $permissionType->value,
+        );
+
+        return CategoryTreeRestrictionFilter::filter($category->getCategoryTree(), $allowed);
+    }
+
     #[Route(path: '/faqs', name: 'admin.faqs', methods: ['GET'])]
     public function index(Request $request): Response
     {
         ...
-        $allowedCategories = $this->currentUser->perm->getAllowedCategoriesForRight(
-            $this->currentUser->getUserId(),
-            PermissionType::FAQ_EDIT->value,
-        );
-
         return $this->render('`@admin/content/faq.overview.twig`', [
             ...
-            'categories' => CategoryTreeRestrictionFilter::filter($category->getCategoryTree(), $allowedCategories),
+            'categories' => $this->getFilteredCategoryTree($category, PermissionType::FAQ_EDIT),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@phpmyfaq/src/phpMyFAQ/Controller/Administration/FaqController.php` around
lines 100 - 110, Extract the repeated permission lookup and category-tree
filtering into a private FaqController helper named
getFilteredCategoryTree(Category $category, PermissionType $permissionType):
array. Have it call getAllowedCategoriesForRight() with the current user ID and
permission value, then pass those categories and $category->getCategoryTree() to
CategoryTreeRestrictionFilter::filter(); replace all seven duplicated patterns
with this helper while preserving each site’s permission type.

Source: Path instructions

tests/phpMyFAQ/Controller/Administration/CategoryControllerTest.php (1)

646-652: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

This PR adds category-scoped permission enforcement to Administration/CategoryController.php (addChild, create, edit, translate, update) and presumably to Administration/FaqController.php. The corresponding admin test files only extend their permission mocks to satisfy the PermissionInterface contract (hasPermissionForCategory() always true, getAllowedCategoriesForRight() always null), unlike the sibling API test files, which added explicit sentinel-category (666) forbidden-path tests in this same PR.

  • tests/phpMyFAQ/Controller/Administration/CategoryControllerTest.php#L646-L652: Add a forbidden-path test for at least one of addChild, create, edit, translate, or update, following the same sentinel-category pattern used in tests/phpMyFAQ/Controller/Administration/Api/CategoryControllerTest.php's testDeleteReturnsForbiddenForRestrictedCategory.
  • tests/phpMyFAQ/Controller/Administration/FaqControllerTest.php#L352-L358: Add an equivalent forbidden-path test for the corresponding Administration/FaqController.php methods that now call userHasPermissionForCategories(), following the pattern in tests/phpMyFAQ/Controller/Administration/Api/FaqControllerTest.php.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/phpMyFAQ/Controller/Administration/CategoryControllerTest.php` around
lines 646 - 652, Add sentinel-category forbidden-path coverage for the
administration category actions in
tests/phpMyFAQ/Controller/Administration/CategoryControllerTest.php:646-652,
using the existing permission mock and pattern from the API category test; the
test should verify a restricted category returns forbidden for at least one of
addChild, create, edit, translate, or update. Add equivalent coverage in
tests/phpMyFAQ/Controller/Administration/FaqControllerTest.php:352-358 for an
affected FAQ action, following the API FAQ test pattern and asserting forbidden
access for category 666.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/FaqController.php`:
- Around line 360-364: Fix the inferred category ID type used by
userHasPermissionForCategories at all four call sites in FaqController,
including the paths around the current category relation and the other
getCategories calls. Prefer adding a precise return annotation to
Relation::getCategories so array_keys infers integer IDs consistently; otherwise
explicitly cast each array_keys result to int[] before passing or spreading it,
while preserving the existing permission checks.

In `@phpmyfaq/src/phpMyFAQ/Controller/Administration/FaqController.php`:
- Around line 303-308: Update the call to userHasPermissionForCategories() in
the FAQ edit flow to convert every key from $categories to an integer before
passing the resulting list, satisfying the method’s array<int> value contract
while leaving the surrounding permission and logging behavior unchanged.

In `@phpmyfaq/src/phpMyFAQ/Permission/MediumPermission.php`:
- Around line 576-627: Update getAllowedCategoriesForRight to aggregate category
permissions using the same checkUserGroupRightForCategory semantics as
hasPermissionForCategory, including categories explicitly present in each
group-right restriction instead of omitting restricted groups. Preserve
unrestricted handling for groups without category restrictions and ensure the
returned set matches the categories that userHasPermissionForCategories can
allow.

---

Outside diff comments:
In `@phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/CategoryController.php`:
- Around line 150-185: Update updateOrder to validate every category node in
categoryTree against the current user’s CATEGORY_EDIT rights before calling
setCategoryTree or updateParentCategory. Reject unauthorized nodes or remove
them from the submitted tree, while preserving authorized nodes and ensuring
parent lookup and persistence use only the validated tree.

In `@phpmyfaq/src/phpMyFAQ/Controller/Administration/CategoryController.php`:
- Around line 69-118: Restrict category data in both index() and hierarchy() to
the current user’s permitted category IDs. Replace the unrestricted category
loading around Category and buildCategoryTree() with the established
permission-scoped category list used by FAQ admin controllers, then filter
categoryTree and categoryInfo-derived categoryTranslations consistently before
rendering.

---

Nitpick comments:
In `@phpmyfaq/src/phpMyFAQ/Controller/Administration/FaqController.php`:
- Around line 100-110: Extract the repeated permission lookup and category-tree
filtering into a private FaqController helper named
getFilteredCategoryTree(Category $category, PermissionType $permissionType):
array. Have it call getAllowedCategoriesForRight() with the current user ID and
permission value, then pass those categories and $category->getCategoryTree() to
CategoryTreeRestrictionFilter::filter(); replace all seven duplicated patterns
with this helper while preserving each site’s permission type.

In `@phpmyfaq/src/phpMyFAQ/Permission/MediumPermission.php`:
- Around line 588-627: Extract the duplicated right-resolution logic into a
private resolveRightId(mixed $right): int helper, preserving the existing
numeric, string, and PermissionType handling. Replace the corresponding
resolution blocks in hasPermission, hasPermissionForCategory, and
getAllowedCategoriesForRight with calls to this helper, then use its returned
integer right ID.

In `@tests/phpMyFAQ/Controller/Administration/CategoryControllerTest.php`:
- Around line 646-652: Add sentinel-category forbidden-path coverage for the
administration category actions in
tests/phpMyFAQ/Controller/Administration/CategoryControllerTest.php:646-652,
using the existing permission mock and pattern from the API category test; the
test should verify a restricted category returns forbidden for at least one of
addChild, create, edit, translate, or update. Add equivalent coverage in
tests/phpMyFAQ/Controller/Administration/FaqControllerTest.php:352-358 for an
affected FAQ action, following the API FAQ test pattern and asserting forbidden
access for category 666.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 584d4ac0-40a1-4536-93a5-d431be2c0b14

📥 Commits

Reviewing files that changed from the base of the PR and between d81c899 and b8b2a45.

📒 Files selected for processing (19)
  • docs/administration.md
  • phpmyfaq/src/phpMyFAQ/Category/CategoryTreeRestrictionFilter.php
  • phpmyfaq/src/phpMyFAQ/Controller/AbstractController.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/CategoryController.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/FaqController.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/CategoryController.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/FaqController.php
  • phpmyfaq/src/phpMyFAQ/Permission/BasicPermission.php
  • phpmyfaq/src/phpMyFAQ/Permission/MediumPermission.php
  • phpmyfaq/src/phpMyFAQ/Permission/PermissionInterface.php
  • tests/phpMyFAQ/Administration/AdminMenuBuilderTest.php
  • tests/phpMyFAQ/Attachment/AttachmentServiceTest.php
  • tests/phpMyFAQ/Category/CategoryTreeRestrictionFilterTest.php
  • tests/phpMyFAQ/Controller/Administration/Api/CategoryControllerTest.php
  • tests/phpMyFAQ/Controller/Administration/Api/FaqControllerTest.php
  • tests/phpMyFAQ/Controller/Administration/CategoryControllerTest.php
  • tests/phpMyFAQ/Controller/Administration/FaqControllerTest.php
  • tests/phpMyFAQ/Permission/BasicPermissionTest.php
  • tests/phpMyFAQ/Permission/MediumPermissionTest.php

Comment thread phpmyfaq/src/phpMyFAQ/Controller/Administration/FaqController.php
Comment thread phpmyfaq/src/phpMyFAQ/Permission/MediumPermission.php
@thorsten
thorsten force-pushed the feat/4311-category-permission-enforcement branch from f04732a to abc49af Compare August 10, 2026 10:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/CategoryController.php`:
- Around line 171-174: Replace the hard-coded denial text in the category
reorder permission check with a new translation key, and retrieve the localized
message through Translation::get() before constructing ForbiddenException. Add
the key to the appropriate translation resources while preserving the permission
name interpolation and existing exception behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb379bb5-12e5-4408-90c9-bb4cbe7257de

📥 Commits

Reviewing files that changed from the base of the PR and between 64af5e1 and abc49af.

📒 Files selected for processing (9)
  • phpmyfaq/src/phpMyFAQ/Category/CategoryTreeRestrictionFilter.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/CategoryController.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/CategoryController.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/FaqController.php
  • phpmyfaq/src/phpMyFAQ/Permission/MediumPermission.php
  • tests/phpMyFAQ/Category/CategoryTreeRestrictionFilterTest.php
  • tests/phpMyFAQ/Controller/Administration/Api/CategoryControllerTest.php
  • tests/phpMyFAQ/Controller/Administration/CategoryControllerTest.php
  • tests/phpMyFAQ/Controller/Administration/FaqControllerTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/FaqController.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/phpMyFAQ/Permission/MediumPermissionTest.php`:
- Around line 713-714: Replace the raw UPDATE and DELETE statements in the test
setup with the existing database abstraction or fixture helper that configures
user 1 as a non-superadmin without right 1. Preserve the resulting permission
state while removing inline SQL from the test.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f2911d0e-caad-4dc5-9fb7-04197274a82d

📥 Commits

Reviewing files that changed from the base of the PR and between abc49af and 996e9ec.

📒 Files selected for processing (1)
  • tests/phpMyFAQ/Permission/MediumPermissionTest.php

Comment thread tests/phpMyFAQ/Permission/MediumPermissionTest.php
@thorsten
thorsten merged commit c43e830 into main Aug 10, 2026
17 checks passed
@thorsten
thorsten deleted the feat/4311-category-permission-enforcement branch August 10, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant