Skip to content

fix: enforce wiki space access on routed pages#597

Open
nkarkare wants to merge 1 commit intofrappe:developfrom
nkarkare:fix/wiki-space-route-access
Open

fix: enforce wiki space access on routed pages#597
nkarkare wants to merge 1 commit intofrappe:developfrom
nkarkare:fix/wiki-space-route-access

Conversation

@nkarkare
Copy link
Copy Markdown

@nkarkare nkarkare commented Apr 10, 2026

Summary

  • enforce wiki space role allowlists on direct routed page access
  • make legacy Wiki Page routes respect the page-level allow_guest flag instead of the DocType-wide allow_guest_to_view shortcut
  • add a standard roles table to Wiki Space so the allowlist is visible/configurable in the model, and cover the new checks with tests

Problem

Direct wiki routes could bypass the access expectations set in the wiki UI:

  • legacy Wiki Page routes were effectively public because allow_guest_to_view short-circuited route permission checks before page-level allow_guest was evaluated
  • neither legacy Wiki Page routes nor v3 Wiki Document routes enforced a wiki-space role allowlist on direct page access

That means a page could be hidden from the sidebar or marked non-guest, yet still render for users outside the intended wiki space roles when they visited the route directly.

Fix

  • add shared helpers to resolve the owning Wiki Space for a route and evaluate its configured roles
  • add WikiPage.has_website_permission() and disable the DocType-wide guest bypass so legacy routes now respect:
    1. page publish state
    2. wiki-space roles, when configured
    3. allow_guest
  • extend WikiDocument.check_guest_access() to also enforce wiki-space role allowlists for v3 routes
  • add a standard roles child table to Wiki Space
  • add regression tests for restricted-space access checks on both legacy and v3 paths

Testing

  • python3 -m py_compile wiki/utils.py wiki/wiki/doctype/wiki_page/wiki_page.py wiki/frappe_wiki/doctype/wiki_document/wiki_document.py wiki/wiki/doctype/wiki_space/wiki_space.py wiki/wiki/doctype/wiki_page/test_wiki_page.py wiki/frappe_wiki/doctype/wiki_document/test_wiki_document.py

I could not run the full Frappe test suite locally in this environment because a bench/test runtime is not available here.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added role-based access control for Wiki Spaces; restrict access to users with specific allowed roles.
    • New "Roles" field in Wiki Space configuration to define allowed role allowlists.
  • Bug Fixes

    • Guest access to Wiki Pages is now disabled by default; permission checks validate user roles against Wiki Space role allowlists before granting access.
  • Tests

    • Added tests for role-restricted Wiki Space access and Wiki Page permission scenarios with role-based restrictions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

Walkthrough

This pull request implements role-based access control for Wiki Spaces and Wiki Pages. A new roles field is added to the Wiki Space DocType to store an allowlist of roles with access. Three utility functions are introduced in wiki/utils.py to locate wiki spaces by route, extract allowed roles, and verify user access. The WikiDocument.check_guest_access() method and a new WikiPage.has_website_permission() method now enforce wiki space access checks. The Wiki Page DocType disables guest viewing by default, while new tests validate the role-based permission logic across various scenarios.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: enforce wiki space access on routed pages' directly and clearly describes the main change: enforcing wiki space access control on pages accessed via routes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
wiki/utils.py (1)

39-53: Consider avoiding full-table scan in route resolver hot path.

get_wiki_space_for_route currently loads all Wiki Space rows and sorts in Python per call. For website requests, this can become costly as spaces grow; a prefix-candidate lookup (depth-based) avoids O(N) scans.

⚡ Suggested approach (depth-based lookup)
 def get_wiki_space_for_route(route: str, published_only: bool = False):
@@
-	normalized_route = route.strip("/")
-	spaces = frappe.get_all("Wiki Space", fields=["name", "route", "is_published"])
-
-	for space in sorted(spaces, key=lambda row: len((row.route or "").strip("/")), reverse=True):
-		space_route = (space.route or "").strip("/")
-		if not space_route:
-			continue
-
-		if normalized_route != space_route and not normalized_route.startswith(f"{space_route}/"):
-			continue
-
-		if published_only and not cint(space.is_published):
-			continue
-
-		return frappe.get_cached_doc("Wiki Space", space.name)
+	normalized_route = route.strip("/")
+	parts = normalized_route.split("/")
+	for i in range(len(parts), 0, -1):
+		candidate = "/".join(parts[:i])
+		filters = {"route": candidate}
+		if published_only:
+			filters["is_published"] = 1
+		space_name = frappe.db.get_value("Wiki Space", filters, "name")
+		if space_name:
+			return frappe.get_cached_doc("Wiki Space", space_name)
 
 	return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wiki/utils.py` around lines 39 - 53, get_wiki_space_for_route currently does
a full-table scan via frappe.get_all("Wiki Space") and sorts in Python; change
it to a depth-based prefix lookup to avoid O(N) per-request work: compute
normalized_route and its path segments, then iterate from the longest prefix to
shortest building candidate_route (e.g., join first i segments), and for each
candidate call frappe.get_all or frappe.get_value filtering by
route=candidate_route (and is_published when published_only) to find a match;
when found return frappe.get_cached_doc("Wiki Space", <name>); keep references
to normalized_route, published_only, and use frappe.get_cached_doc for the
returned record.
wiki/frappe_wiki/doctype/wiki_document/test_wiki_document.py (1)

178-212: Make user restoration failure-safe in the new tests.

Use self.addCleanup(...) (or try/finally) after frappe.set_user(...) so session user is always restored, even if an assertion fails mid-test.

✅ Suggested hardening
 		with (
 			patch.object(doc, "get_wiki_space", return_value=frappe._dict(name="restricted-space")),
 			patch(
 				"wiki.frappe_wiki.doctype.wiki_document.wiki_document.frappe.get_cached_doc",
 				return_value=fake_space,
 			),
 		):
 			frappe.set_user(user.name)
+			self.addCleanup(frappe.set_user, "Administrator")
 			with self.assertRaises(frappe.PermissionError):
 				doc.check_guest_access()
-
-		frappe.set_user("Administrator")
@@
 		with (
 			patch.object(doc, "get_wiki_space", return_value=frappe._dict(name="restricted-space")),
 			patch(
 				"wiki.frappe_wiki.doctype.wiki_document.wiki_document.frappe.get_cached_doc",
 				return_value=fake_space,
 			),
 		):
 			frappe.set_user(user.name)
+			self.addCleanup(frappe.set_user, "Administrator")
 			doc.check_guest_access()
-
-		frappe.set_user("Administrator")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wiki/frappe_wiki/doctype/wiki_document/test_wiki_document.py` around lines
178 - 212, The tests test_role_restricted_space_blocks_user_without_allowed_role
and test_role_restricted_space_allows_user_with_allowed_role call
frappe.set_user(...) but do not guarantee restoration if an assertion raises;
update each test to restore the session user safely by registering
self.addCleanup(lambda: frappe.set_user("Administrator")) immediately after
calling frappe.set_user(user.name) (or wrap the patch/context block in
try/finally and call frappe.set_user("Administrator") in finally). Ensure the
change is applied around the calls to doc.check_guest_access() and references to
frappe.set_user and doc.check_guest_access so the session is always restored
even on failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wiki/wiki/doctype/wiki_page/wiki_page.py`:
- Around line 30-41: In has_website_permission, normalize the user value before
the final guest check so that None cannot bypass access control; e.g., ensure
user is treated as a non-authenticated guest by handling user is None (or
coercing user = user or "Guest") and then perform the existing allow_guest and
guest comparison logic (affecting the existing return user != "Guest" behavior)
so pages don't incorrectly allow access when user is None.

---

Nitpick comments:
In `@wiki/frappe_wiki/doctype/wiki_document/test_wiki_document.py`:
- Around line 178-212: The tests
test_role_restricted_space_blocks_user_without_allowed_role and
test_role_restricted_space_allows_user_with_allowed_role call
frappe.set_user(...) but do not guarantee restoration if an assertion raises;
update each test to restore the session user safely by registering
self.addCleanup(lambda: frappe.set_user("Administrator")) immediately after
calling frappe.set_user(user.name) (or wrap the patch/context block in
try/finally and call frappe.set_user("Administrator") in finally). Ensure the
change is applied around the calls to doc.check_guest_access() and references to
frappe.set_user and doc.check_guest_access so the session is always restored
even on failures.

In `@wiki/utils.py`:
- Around line 39-53: get_wiki_space_for_route currently does a full-table scan
via frappe.get_all("Wiki Space") and sorts in Python; change it to a depth-based
prefix lookup to avoid O(N) per-request work: compute normalized_route and its
path segments, then iterate from the longest prefix to shortest building
candidate_route (e.g., join first i segments), and for each candidate call
frappe.get_all or frappe.get_value filtering by route=candidate_route (and
is_published when published_only) to find a match; when found return
frappe.get_cached_doc("Wiki Space", <name>); keep references to
normalized_route, published_only, and use frappe.get_cached_doc for the returned
record.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 94705e04-2dd4-4e84-adc5-c6543fc80a1f

📥 Commits

Reviewing files that changed from the base of the PR and between 0421984 and ea34aea.

📒 Files selected for processing (8)
  • wiki/frappe_wiki/doctype/wiki_document/test_wiki_document.py
  • wiki/frappe_wiki/doctype/wiki_document/wiki_document.py
  • wiki/utils.py
  • wiki/wiki/doctype/wiki_page/test_wiki_page.py
  • wiki/wiki/doctype/wiki_page/wiki_page.json
  • wiki/wiki/doctype/wiki_page/wiki_page.py
  • wiki/wiki/doctype/wiki_space/wiki_space.json
  • wiki/wiki/doctype/wiki_space/wiki_space.py

Comment on lines +30 to +41
def has_website_permission(self, ptype, user, verbose=False):
if not self.published:
return False

wiki_space = get_wiki_space_for_route(self.route)
if wiki_space and not has_wiki_space_access(wiki_space, user=user):
return False

if self.allow_guest:
return True

return user != "Guest"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize user before guest check to avoid None bypass.

On Line 41, user=None currently passes the final check (None != "Guest"), which can incorrectly allow access on non-guest pages.

🔧 Proposed fix
-def has_website_permission(self, ptype, user, verbose=False):
+def has_website_permission(self, ptype, user=None, verbose=False):
+	user = user or frappe.session.user
 	if not self.published:
 		return False

 	wiki_space = get_wiki_space_for_route(self.route)
 	if wiki_space and not has_wiki_space_access(wiki_space, user=user):
 		return False

 	if self.allow_guest:
 		return True

 	return user != "Guest"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def has_website_permission(self, ptype, user, verbose=False):
if not self.published:
return False
wiki_space = get_wiki_space_for_route(self.route)
if wiki_space and not has_wiki_space_access(wiki_space, user=user):
return False
if self.allow_guest:
return True
return user != "Guest"
def has_website_permission(self, ptype, user=None, verbose=False):
user = user or frappe.session.user
if not self.published:
return False
wiki_space = get_wiki_space_for_route(self.route)
if wiki_space and not has_wiki_space_access(wiki_space, user=user):
return False
if self.allow_guest:
return True
return user != "Guest"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wiki/wiki/doctype/wiki_page/wiki_page.py` around lines 30 - 41, In
has_website_permission, normalize the user value before the final guest check so
that None cannot bypass access control; e.g., ensure user is treated as a
non-authenticated guest by handling user is None (or coercing user = user or
"Guest") and then perform the existing allow_guest and guest comparison logic
(affecting the existing return user != "Guest" behavior) so pages don't
incorrectly allow access when user is None.

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