Blame

36fe86 Claude (MCP) 2026-03-17 03:12:13
[mcp] Plan: Permissions panel re-enablement implementation
1
---
2
category: plan
3
tags: [implementation, permissions, admin, multi-tenancy]
4
last_updated: 2026-03-17
5
confidence: high
6
---
7
8
# Permissions Panel Re-enablement: Implementation Plan
9
10
Parent: [[Design/Admin_Panel_Reenablement]]
11
12
## Summary
13
14
Re-enable the Permissions admin panel (`/-/admin/permissions_and_registration`) in robot.wtf's multi-tenant platform. Wiki owners can set READ_ACCESS, WRITE_ACCESS, ATTACHMENT_ACCESS per-wiki. The resolver intersects these with platform ACL permissions before injecting proxy headers.
15
16
## Repos and Branches
17
18
| Repo | Base branch | Feature branch |
19
|------|------------|----------------|
20
| `otterwiki` (`/Users/sderle/code/otterwiki/otterwiki/`) | `wikibot-io` | `feat/permissions-panel` |
21
| `robot.wtf` (`/Users/sderle/code/otterwiki/robot.wtf/`) | `main` | `feat/permissions-panel` |
22
23
Changes can be developed and tested independently. The otterwiki changes are purely UI (template + decorator removal). The robot.wtf changes are purely resolver logic with no otterwiki import changes.
24
25
## Change 1: Otterwiki fork — Remove decorator and hide registration fields
26
27
### 1a. Remove `@platform_mode_disabled` from the route
28
29
**File:** `/Users/sderle/code/otterwiki/otterwiki/otterwiki/views.py`, lines 237–248
30
31
Current:
32
```python
33
@app.route(
34
"/-/admin/permissions_and_registration", methods=["POST", "GET"]
35
) # pyright: ignore -- false positive
36
@login_required
37
@platform_mode_disabled
38
def admin_permissions_and_registration():
39
```
40
41
Change: Remove `@platform_mode_disabled` line (line 241).
42
43
### 1b. Show sidebar link in PLATFORM_MODE for permissions only
44
45
**File:** `/Users/sderle/code/otterwiki/otterwiki/otterwiki/templates/settings.html`, lines 43–62
46
47
Currently the entire block (User Management, Permissions, Mail) is gated by `{% if not config.PLATFORM_MODE %}`. Change to:
48
- Keep User Management and Mail inside the `{% if not config.PLATFORM_MODE %}` guard
49
- Move the Permissions link outside it (before the guard, after Repository Management)
50
51
New structure (lines ~42–62):
52
```jinja
53
<a href="{{ url_for("admin_permissions_and_registration") }}" class="sidebar-link sidebar-link-with-icon">
54
<span class="sidebar-icon" style="min-width: 3rem;">
55
<i class="fas fa-users-cog"></i>
56
</span>
57
Permissions{% if not config.PLATFORM_MODE %} and Registration{% endif %}
58
</a>
59
{% if not config.PLATFORM_MODE %}
60
<a href="{{ url_for("admin_user_management") }}" ...>
61
User Management
62
</a>
63
<a href="{{ url_for("admin_mail_preferences") }}" ...>
64
Mail Preferences
65
</a>
66
{% endif %}
67
```
68
69
### 1c. Hide registration fields in PLATFORM_MODE
70
71
**File:** `/Users/sderle/code/otterwiki/otterwiki/otterwiki/templates/admin/permissions_and_registration.html`
72
73
Wrap lines 34–85 (the five registration checkboxes: DISABLE_REGISTRATION, EMAIL_NEEDS_CONFIRMATION, AUTO_APPROVAL, NOTIFY_ADMINS_ON_REGISTER, NOTIFY_USER_ON_APPROVAL) in:
74
75
```jinja
76
{% if not config.PLATFORM_MODE %}
77
{# ... all five checkbox form groups ... #}
78
{% endif %}
79
```
80
81
Also hide the "ADMIN" option from the access level dropdowns in PLATFORM_MODE (line 24). In platform mode, ADMIN-level access restriction doesn't apply since admin is determined by ACL role, not registration. Change the loop:
82
83
```jinja
84
{% for permission_option in ["ANONYMOUS","REGISTERED","APPROVED"] + ([] if config.PLATFORM_MODE else ["ADMIN"]) %}
85
```
86
87
### 1d. Hide the per-user note about setting to "Admin"
88
89
Line 10–12 in the template contains a note about setting access to "Admin" and using User Management. In PLATFORM_MODE, wrap this in `{% if not config.PLATFORM_MODE %}...{% endif %}` and show an alternative note:
90
91
```jinja
92
{% if config.PLATFORM_MODE %}
93
<div>
94
<em>Note:</em> These settings control per-wiki access levels. The platform ACL (owner/editor/viewer)
95
grants the maximum permissions. These settings can restrict further but never escalate.
96
"Anonymous" means anyone with a link. "Registered" means authenticated platform users.
97
</div>
98
{% else %}
99
<div>
100
<em>Note:</em> To configure privileges per user, ...
101
</div>
102
{% endif %}
103
```
104
105
### 1e. Handle form submission for PLATFORM_MODE
106
107
**File:** `/Users/sderle/code/otterwiki/otterwiki/otterwiki/preferences.py`, lines 419–436 (`handle_permissions_and_registration`)
108
109
No changes needed. The function already saves only the values present in the form. Since registration checkboxes will be absent in PLATFORM_MODE, they won't be updated. However, on first save the missing checkboxes will be saved as "False" (their default from `form.get(checkbox, "False")`).
110
111
**Fix:** Guard the registration checkbox loop with a PLATFORM_MODE check:
112
113
```python
114
def handle_permissions_and_registration(form):
115
for name in ["READ_access", "WRITE_access", "ATTACHMENT_access"]:
116
_update_preference(name.upper(), form.get(name, "ANONYMOUS"))
117
if not app.config.get("PLATFORM_MODE"):
118
for checkbox in [
119
"disable_registration",
120
"auto_approval",
121
"email_needs_confirmation",
122
"notify_admins_on_register",
123
"notify_user_on_approval",
124
]:
125
_update_preference(checkbox.upper(), form.get(checkbox, "False"))
126
db.session.commit()
127
update_app_config()
128
toast("Preferences updated.")
129
return redirect(url_for("admin_permissions_and_registration"))
130
```
131
132
## Change 2: robot.wtf resolver — Intersect permissions with per-wiki access levels
133
134
### 2a. New function: `_apply_wiki_access_restrictions`
135
136
**File:** `/Users/sderle/code/otterwiki/robot.wtf/app/resolver.py`
137
138
Add after `_is_write_request` (around line 273):
139
140
```python
141
def _apply_wiki_access_restrictions(
142
permissions: tuple[str, ...] | list[str],
143
is_authenticated: bool,
144
) -> list[str]:
145
"""Intersect ACL-granted permissions with per-wiki access preferences.
146
147
After _swap_database() + update_app_config(), the per-wiki
148
READ_ACCESS / WRITE_ACCESS / ATTACHMENT_ACCESS values are in app.config.
149
150
Platform ACL grants the ceiling. Per-wiki preferences can only restrict.
151
152
Access levels:
153
ANONYMOUS — no restriction (anyone gets this permission)
154
REGISTERED — only authenticated users
155
APPROVED — treated same as REGISTERED (no per-user approval tracking yet)
156
157
Args:
158
permissions: ACL-granted permissions (e.g. ("READ", "WRITE", "UPLOAD"))
159
is_authenticated: Whether the request has a resolved identity
160
(not anonymous)
161
162
Returns:
163
Filtered list of permissions.
164
"""
165
import otterwiki.server
166
app = otterwiki.server.app
167
168
result = list(permissions)
169
170
access_map = {
171
"READ": "READ_ACCESS",
172
"WRITE": "WRITE_ACCESS",
173
"UPLOAD": "ATTACHMENT_ACCESS",
174
}
175
176
for perm, config_key in access_map.items():
177
if perm not in result:
178
continue
179
level = app.config.get(config_key, "ANONYMOUS").upper()
180
if level == "ANONYMOUS":
181
continue
182
# REGISTERED and APPROVED both require authentication
183
# (APPROVED would require per-user tracking; deferred — treat as REGISTERED)
184
if level in ("REGISTERED", "APPROVED", "ADMIN"):
185
if not is_authenticated:
186
result.remove(perm)
187
188
# WRITE requires READ; UPLOAD requires WRITE
189
if "READ" not in result:
190
for dep in ("WRITE", "UPLOAD"):
191
if dep in result:
192
result.remove(dep)
193
if "WRITE" not in result:
194
if "UPLOAD" in result:
195
result.remove("UPLOAD")
196
197
return result
198
```
199
200
### 2b. Call `_apply_wiki_access_restrictions` in `TenantResolver.__call__`
201
202
**File:** `/Users/sderle/code/otterwiki/robot.wtf/app/resolver.py`, lines 396–406 (after `_swap_database`, before injecting headers)
203
204
Insert between `_swap_database(wiki_dir)` (line 399) and the header injection block (line 402):
205
206
```python
207
# Apply per-wiki access restrictions
208
proxy_headers = auth_result["proxy_headers"]
209
is_authenticated = proxy_headers.get("x-otterwiki-email", "") not in (
210
"", "@anonymous"
211
)
212
perms_key = "X-Otterwiki-Permissions" # Note: key casing matches build_proxy_headers
213
# Actually the key is lowercase in build_proxy_headers
214
perms_key = "x-otterwiki-permissions"
215
raw_perms = proxy_headers.get(perms_key, "").split(",")
216
restricted = _apply_wiki_access_restrictions(raw_perms, is_authenticated)
217
proxy_headers[perms_key] = format_permission_header(restricted)
218
```
219
220
**Important:** This must happen AFTER `_swap_database()` (which calls `update_app_config()`, loading per-wiki preferences into `app.config`) and BEFORE injecting headers into environ.
221
222
The existing quota-stripping logic (lines 382–388) should remain and runs on the already-restricted permissions. Reorder: wiki access restrictions first, then quota stripping. Or just let both run — quota stripping is additive removal and safe to apply on already-restricted perms.
223
224
### 2c. Determine `is_authenticated` from auth path
225
226
Looking at the auth resolution paths:
227
- JWT auth → authenticated (email = `@{handle}`)
228
- Cookie auth → authenticated (email = `@{handle}`)
229
- Bearer token → authenticated (email = `mcp@robot.wtf`)
230
- API key → authenticated (email = `@system`)
231
- Anonymous → NOT authenticated (email = `@anonymous`)
232
233
The check `email not in ("", "@anonymous")` correctly identifies unauthenticated requests.
234
235
## The APPROVED Level Question
236
237
**Can we support APPROVED without per-user tracking?** No — not fully.
238
239
- In vanilla Otterwiki, APPROVED means users must be individually approved by an admin via User Management.
240
- In robot.wtf, there's no per-wiki user table being actively populated (users are transient proxy-header objects).
241
- User Management panel is still disabled.
242
243
**Decision: Treat APPROVED the same as REGISTERED for now.** Both mean "authentication required." This is documented in the function docstring. When User Management is re-enabled (Phase 2 per [[Design/Admin_Panel_Reenablement]]), APPROVED can be differentiated.
244
245
**UI consideration:** We could hide the APPROVED option in PLATFORM_MODE, but it's better to leave it visible and documented. Wiki owners who set APPROVED get REGISTERED behavior — a reasonable default that becomes stricter (not looser) when user tracking lands.
246
247
## What Happens for Each User Type
248
249
| User type | Platform ACL perms | Wiki READ_ACCESS=ANON | Wiki READ_ACCESS=REGISTERED | Wiki WRITE_ACCESS=REGISTERED |
250
|-----------|-------------------|----------------------|---------------------------|------------------------------|
251
| Owner (JWT) | READ,WRITE,UPLOAD,ADMIN | all | all | all |
252
| Editor (JWT/cookie) | READ,WRITE,UPLOAD | all | all | all |
253
| Viewer (ACL) | READ | READ only | READ only | READ only |
254
| Public (anon, wiki is_public) | READ | READ | **none** (stripped) | READ only |
255
| MCP bearer token | READ,WRITE,UPLOAD | all | all | all |
256
| No auth, wiki private | 403 before reaching this code | N/A | N/A | N/A |
257
258
Key insight: When READ_ACCESS=REGISTERED, anonymous users on a public wiki lose READ. This effectively makes the wiki private despite `is_public=true` in the platform DB. That's correct — wiki owner's preference is the final word on restrictions.
259
260
## ADMIN Permission Pass-Through
261
262
The `_apply_wiki_access_restrictions` function does NOT touch the ADMIN permission. ADMIN is purely platform-controlled (ACL role = owner). The wiki-level access preferences only affect READ, WRITE, and UPLOAD.
263
264
## Test Specifications
265
266
### Otterwiki fork tests
267
268
**File:** New test file or extend existing admin test.
269
270
1. **Route accessible in PLATFORM_MODE:** With `PLATFORM_MODE=True` and an admin user, GET `/-/admin/permissions_and_registration` returns 200 (not 404).
271
2. **Registration fields hidden:** Response HTML does not contain `disable_registration` checkbox when `PLATFORM_MODE=True`.
272
3. **Registration fields shown:** Response HTML contains `disable_registration` when `PLATFORM_MODE=False`.
273
4. **ADMIN option hidden in PLATFORM_MODE:** The access dropdowns don't include "ADMIN" option when `PLATFORM_MODE=True`.
274
5. **Form submission saves only access prefs in PLATFORM_MODE:** POST with READ_access=REGISTERED saves READ_ACCESS but does not touch DISABLE_REGISTRATION.
275
6. **Sidebar link visible in PLATFORM_MODE:** The settings template renders a link to `admin_permissions_and_registration` when `PLATFORM_MODE=True`.
276
277
### robot.wtf tests
278
279
**File:** `/Users/sderle/code/otterwiki/robot.wtf/tests/test_resolver.py` (extend)
280
281
Unit tests for `_apply_wiki_access_restrictions`:
282
283
1. **All ANONYMOUS — no restriction:** Permissions pass through unchanged.
284
2. **READ_ACCESS=REGISTERED, authenticated user:** READ preserved.
285
3. **READ_ACCESS=REGISTERED, anonymous user:** READ stripped; WRITE and UPLOAD also stripped (dependency).
286
4. **WRITE_ACCESS=REGISTERED, anonymous user with READ_ACCESS=ANONYMOUS:** READ preserved, WRITE stripped, UPLOAD stripped.
287
5. **ATTACHMENT_ACCESS=REGISTERED, anonymous user:** READ and WRITE preserved, UPLOAD stripped.
288
6. **APPROVED treated as REGISTERED:** READ_ACCESS=APPROVED, anonymous → READ stripped.
289
7. **ADMIN permission not touched:** Owner with ADMIN + READ_ACCESS=REGISTERED, anonymous → ADMIN preserved (though this case shouldn't occur in practice since ADMIN requires auth).
290
8. **Dependency chain:** WRITE removed → UPLOAD also removed even if ATTACHMENT_ACCESS=ANONYMOUS.
291
292
Integration-level tests (mock `_swap_database` and `otterwiki.server.app.config`):
293
294
9. **Full resolver flow:** Authenticated cookie user on wiki with WRITE_ACCESS=REGISTERED gets WRITE permission.
295
10. **Full resolver flow:** Anonymous user on public wiki with READ_ACCESS=REGISTERED gets no permissions (empty header).
296
11. **Quota stripping + access restriction compose correctly.**
297
298
## Deployment and Independence
299
300
The two feature branches can be merged independently:
301
302
- **otterwiki fork first:** Safe to merge. The `@platform_mode_disabled` removal means the route returns the form, but without resolver-side intersection, the saved preferences have no effect on actual access control. The proxy header permissions still come from the ACL alone. This is harmless — the admin can save preferences that don't do anything yet.
303
304
- **robot.wtf first:** Safe to merge. The resolver reads `app.config` values for READ_ACCESS etc. If the preferences haven't been set (no admin panel yet), the defaults are all "ANONYMOUS" (from `otterwiki/server.py` line 38–40), so no permissions are stripped. No behavioral change.
305
306
**Recommended order:** otterwiki fork first (UI changes, simpler to test), then robot.wtf (logic changes, needs more test coverage).
307
308
## Open Questions
309
310
1. **Should we log when wiki-level restrictions strip permissions?** Probably yes, at DEBUG level, for troubleshooting.
311
2. **Should the platform management UI (robot.wtf dashboard) expose these settings too?** Not now — the otterwiki admin panel is sufficient.
312
3. **When READ_ACCESS=REGISTERED strips READ from anonymous on a public wiki, should the resolver return 403 instead of passing through with empty permissions?** Current design passes through with empty permissions; otterwiki will show appropriate "access denied" UI. This is probably fine.