Created
October 24, 2025 18:02
-
-
Save simonw/fda40ff7293eab5eb5f94798ddad99bb to your computer and use it in GitHub Desktop.
Terminal output HTML
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <pre style="background: rgb(0, 0, 0); color: rgb(242, 242, 242); padding: 15px; border-radius: 4px;"><span style="color: rgb(242, 242, 242);">> </span><span style="color: rgb(255, 255, 255); background: rgb(42, 42, 42);">Take a look at https://github.com/simonw/datasette/issues/2509 and </span><span style="color: rgb(227, 71, 70);">u</span><span style="color: rgb(240, 118, 70);">l</span><span style="color: rgb(247, 183, 77);">t</span><span style="color: rgb(129, 191, 111);">r</span><span style="color: rgb(112, 152, 212);">a</span><span style="color: rgb(136, 107, 188);">t</span><span style="color: rgb(186, 107, 165);">h</span><span style="color: rgb(227, 71, 70);">i</span><span style="color: rgb(240, 118, 70);">n</span><span style="color: rgb(247, 183, 77);">k</span><span style="color: rgb(255, 255, 255); background: rgb(42, 42, 42);"> about what is going on</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Fetch</span><span style="color: rgb(242, 242, 242);">(https://github.com/simonw/datasette/issues/2509)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Received </span><span style="color: rgb(255, 255, 255);">257.5KB</span><span style="color: rgb(242, 242, 242);"> (200 OK)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Search</span><span style="color: rgb(242, 242, 242);">(pattern: "/-/allowed")</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Found </span><span style="color: rgb(255, 255, 255);">8 </span><span style="color: rgb(242, 242, 242);">files </span><span style="color: rgb(97, 97, 97);">(ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Search</span><span style="color: rgb(242, 242, 242);">(pattern: "async def allowed", path: "datasette/app.py", output_mode: "content")</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Found </span><span style="color: rgb(255, 255, 255);">110 </span><span style="color: rgb(242, 242, 242);">lines </span><span style="color: rgb(97, 97, 97);">(ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Search</span><span style="color: rgb(242, 242, 242);">(pattern: "class.*Allowed.*View|def.*allowed.*endpoint", output_mode: "content")</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Found </span><span style="color: rgb(255, 255, 255);">15 </span><span style="color: rgb(242, 242, 242);">lines </span><span style="color: rgb(97, 97, 97);">(ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Read</span><span style="color: rgb(242, 242, 242);">(datasette/views/special.py)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Read </span><span style="color: rgb(255, 255, 255);">100</span><span style="color: rgb(242, 242, 242);"> lines</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Search</span><span style="color: rgb(242, 242, 242);">(pattern: "class AllowedResourcesView", path: "datasette/views/special.py", output_mode: "content")</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Found </span><span style="color: rgb(255, 255, 255);">101 </span><span style="color: rgb(242, 242, 242);">lines </span><span style="color: rgb(97, 97, 97);">(ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Read</span><span style="color: rgb(242, 242, 242);">(datasette/views/special.py)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Read </span><span style="color: rgb(255, 255, 255);">100</span><span style="color: rgb(242, 242, 242);"> lines</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Search</span><span style="color: rgb(242, 242, 242);">(pattern: "def resolve_permissions_from_catalog", output_mode: "content")</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Found </span><span style="color: rgb(255, 255, 255);">163 </span><span style="color: rgb(242, 242, 242);">lines </span><span style="color: rgb(97, 97, 97);">(ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Search</span><span style="color: rgb(242, 242, 242);">(pattern: "settings.*allow|permissions.*settings")</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Found </span><span style="color: rgb(255, 255, 255);">7 </span><span style="color: rgb(242, 242, 242);">files </span><span style="color: rgb(97, 97, 97);">(ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Search</span><span style="color: rgb(242, 242, 242);">(pattern: "def.*permission_allowed|permission_allowed_sql", path: "datasette/app.py", output_mode: "content",</span> | |
| <span style="color: rgb(242, 242, 242);"> head_limit: 100)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Found </span><span style="color: rgb(255, 255, 255);">94 </span><span style="color: rgb(242, 242, 242);">lines </span><span style="color: rgb(97, 97, 97);">(ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Search</span><span style="color: rgb(242, 242, 242);">(pattern: "@pm.hook</span>\<span style="color: rgb(242, 242, 242);">|register.*permission_resources_sql</span>\<span style="color: rgb(242, 242, 242);">|settings.*permission", path: "datasette/app.py",</span> | |
| <span style="color: rgb(242, 242, 242);"> output_mode: "content", head_limit: 50)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Found </span><span style="color: rgb(255, 255, 255);">0 </span><span style="color: rgb(242, 242, 242);">lines </span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Search</span><span style="color: rgb(242, 242, 242);">(pattern: "class.*DefaultPermissions|default_permissions")</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Found </span><span style="color: rgb(255, 255, 255);">9 </span><span style="color: rgb(242, 242, 242);">files </span><span style="color: rgb(97, 97, 97);">(ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Read</span><span style="color: rgb(242, 242, 242);">(datasette/default_permissions.py)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Read </span><span style="color: rgb(255, 255, 255);">567</span><span style="color: rgb(242, 242, 242);"> lines</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Now I understand what's happening! Let me write up my analysis:</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Ultra-thinking on Issue #2509</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);">The Problem</span> | |
| <span style="color: rgb(242, 242, 242);"> When running:</span> | |
| <span style="color: rgb(242, 242, 242);"> datasette content.db --root -s databases.content.allow </span><span style="color: rgb(64, 11, 217);">false</span> | |
| <span style="color: rgb(242, 242, 242);"> - The </span><span style="color: rgb(162, 168, 247);">/-/allowed?action=view-table</span><span style="color: rgb(242, 242, 242);"> endpoint </span><span style="color: rgb(255, 255, 255);">incorrectly</span><span style="color: rgb(242, 242, 242);"> shows content database tables</span> | |
| <span style="color: rgb(242, 242, 242);"> - But accessing </span><span style="color: rgb(162, 168, 247);">/content/repos</span><span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">correctly</span><span style="color: rgb(242, 242, 242);"> returns 403</span> | |
| <span style="color: rgb(242, 242, 242);"> - The </span><span style="color: rgb(162, 168, 247);">/-/rules?action=view-table</span><span style="color: rgb(242, 242, 242);"> endpoint doesn't show the deny rule</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);">Root Cause</span> | |
| <span style="color: rgb(242, 242, 242);"> The issue is in </span><span style="color: rgb(162, 168, 247);">/Users/simon/Dropbox/Development/datasette/datasette/default_permissions.py</span><span style="color: rgb(242, 242, 242);"> at </span><span style="color: rgb(255, 255, 255);">lines 206-220</span><span style="color: rgb(242, 242, 242);"> in the</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(162, 168, 247);">permission_resources_sql</span><span style="color: rgb(242, 242, 242);"> hook:</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">async</span><span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">def</span><span style="color: rgb(159, 160, 28);"> permission_resources_sql(datasette, actor, action):</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(47, 180, 29);"># Root user with root_enabled gets all permissions</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">if</span><span style="color: rgb(242, 242, 242);"> datasette.root_enabled </span><span style="color: rgb(64, 11, 217);">and</span><span style="color: rgb(242, 242, 242);"> actor </span><span style="color: rgb(64, 11, 217);">and</span><span style="color: rgb(242, 242, 242);"> actor.get(</span><span style="color: rgb(180, 36, 25);">"id"</span><span style="color: rgb(242, 242, 242);">) == </span><span style="color: rgb(180, 36, 25);">"root"</span><span style="color: rgb(242, 242, 242);">:</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(47, 180, 29);"># Return SQL that grants access to ALL resources for this action</span> | |
| <span style="color: rgb(242, 242, 242);"> action_obj = datasette.actions.get(action)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">if</span><span style="color: rgb(242, 242, 242);"> action_obj </span><span style="color: rgb(64, 11, 217);">and</span><span style="color: rgb(242, 242, 242);"> action_obj.resource_class:</span> | |
| <span style="color: rgb(242, 242, 242);"> resources_sql = action_obj.resource_class.resources_sql()</span> | |
| <span style="color: rgb(242, 242, 242);"> sql = </span><span style="color: rgb(180, 36, 25);">f"""</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(180, 36, 25);"> SELECT parent, child, 1 AS allow, 'root user' AS reason</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(180, 36, 25);"> FROM (</span><span style="color: rgb(180, 36, 25);">{resources_sql</span><span style="color: rgb(180, 36, 25);">})</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(180, 36, 25);"> """</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">return</span><span style="color: rgb(242, 242, 242);"> PermissionSQL(</span> | |
| <span style="color: rgb(242, 242, 242);"> source=</span><span style="color: rgb(180, 36, 25);">"root_permissions"</span><span style="color: rgb(242, 242, 242);">,</span> | |
| <span style="color: rgb(242, 242, 242);"> sql=sql,</span> | |
| <span style="color: rgb(242, 242, 242);"> params=</span><span style="color: rgb(242, 242, 242);">{</span><span style="color: rgb(242, 242, 242);">},</span> | |
| <span style="color: rgb(242, 242, 242);"> )</span> | |
| <span style="color: rgb(242, 242, 242);"> rules: </span><span style="color: rgb(46, 174, 187);">list</span><span style="color: rgb(242, 242, 242);">[PermissionSQL] = []</span> | |
| <span style="color: rgb(242, 242, 242);"> config_rules = </span><span style="color: rgb(64, 11, 217);">await</span><span style="color: rgb(242, 242, 242);"> _config_permission_rules(datasette, actor, action) </span><span style="color: rgb(47, 180, 29);"># Line 224</span> | |
| <span style="color: rgb(242, 242, 242);"> rules.extend(config_rules)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">The bug</span><span style="color: rgb(242, 242, 242);">: When the actor is the root user, the function </span><span style="color: rgb(255, 255, 255);">returns immediately</span><span style="color: rgb(242, 242, 242);"> at line 220 with a blanket "allow all" rule.</span> | |
| <span style="color: rgb(242, 242, 242);"> It never reaches line 224 where </span><span style="color: rgb(162, 168, 247);">_config_permission_rules()</span><span style="color: rgb(242, 242, 242);"> would read the settings-based deny rules (</span><span style="color: rgb(162, 168, 247);">-s </span> | |
| <span style="color: rgb(242, 242, 242);"> databases.content.allow false).</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);">Why It Works Differently for Direct Access</span> | |
| <span style="color: rgb(242, 242, 242);"> The OLD </span><span style="color: rgb(162, 168, 247);">permission_allowed()</span><span style="color: rgb(242, 242, 242);"> system (used when actually accessing </span><span style="color: rgb(162, 168, 247);">/content/repos</span><span style="color: rgb(242, 242, 242);">) works correctly because:</span> | |
| <span style="color: rgb(242, 242, 242);"> 1. At line 188-201, there's </span><span style="color: rgb(162, 168, 247);">permission_allowed_root()</span><span style="color: rgb(242, 242, 242);"> that grants root all permissions</span> | |
| <span style="color: rgb(242, 242, 242);"> 2. But at line 130-170, there's </span><span style="color: rgb(162, 168, 247);">permission_allowed_sql_bridge()</span><span style="color: rgb(242, 242, 242);"> with </span><span style="color: rgb(162, 168, 247);">@hookimpl(tryfirst=True)</span> | |
| <span style="color: rgb(242, 242, 242);"> 3. The </span><span style="color: rgb(162, 168, 247);">tryfirst=True</span><span style="color: rgb(242, 242, 242);"> means config rules are checked BEFORE the root check</span> | |
| <span style="color: rgb(242, 242, 242);"> 4. So config deny rules can override root privileges in the old system</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);">The Fix</span> | |
| <span style="color: rgb(242, 242, 242);"> The </span><span style="color: rgb(162, 168, 247);">permission_resources_sql</span><span style="color: rgb(242, 242, 242);"> hook should </span><span style="color: rgb(255, 255, 255);">NOT return early</span><span style="color: rgb(242, 242, 242);"> for root users. Instead, it should:</span> | |
| <span style="color: rgb(242, 242, 242);"> 1. Add the root "allow all" rule to the </span><span style="color: rgb(162, 168, 247);">rules</span><span style="color: rgb(242, 242, 242);"> list</span> | |
| <span style="color: rgb(242, 242, 242);"> 2. Then continue to add config rules from settings</span> | |
| <span style="color: rgb(242, 242, 242);"> 3. Return all rules together</span> | |
| <span style="color: rgb(242, 242, 242);"> 4. The SQL cascading logic will properly apply: </span><span style="color: rgb(255, 255, 255);">more specific deny rules beat less specific allow rules</span> | |
| <span style="color: rgb(242, 242, 242);"> This way:</span> | |
| <span style="color: rgb(242, 242, 242);"> - Root gets a global-level allow (</span><span style="color: rgb(162, 168, 247);">parent=NULL, child=NULL</span><span style="color: rgb(242, 242, 242);">)</span> | |
| <span style="color: rgb(242, 242, 242);"> - Settings provide a database-level deny (</span><span style="color: rgb(162, 168, 247);">parent='content', child=NULL</span><span style="color: rgb(242, 242, 242);">)</span> | |
| <span style="color: rgb(242, 242, 242);"> - The database-level deny (depth=1) beats the global allow (depth=0)</span> | |
| <span style="color: rgb(242, 242, 242);"> The fix would be to change lines 206-220 to append rather than return:</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">if</span><span style="color: rgb(242, 242, 242);"> datasette.root_enabled </span><span style="color: rgb(64, 11, 217);">and</span><span style="color: rgb(242, 242, 242);"> actor </span><span style="color: rgb(64, 11, 217);">and</span><span style="color: rgb(242, 242, 242);"> actor.get(</span><span style="color: rgb(180, 36, 25);">"id"</span><span style="color: rgb(242, 242, 242);">) == </span><span style="color: rgb(180, 36, 25);">"root"</span><span style="color: rgb(242, 242, 242);">:</span> | |
| <span style="color: rgb(242, 242, 242);"> action_obj = datasette.actions.get(action)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">if</span><span style="color: rgb(242, 242, 242);"> action_obj </span><span style="color: rgb(64, 11, 217);">and</span><span style="color: rgb(242, 242, 242);"> action_obj.resource_class:</span> | |
| <span style="color: rgb(242, 242, 242);"> resources_sql = action_obj.resource_class.resources_sql()</span> | |
| <span style="color: rgb(242, 242, 242);"> sql = </span><span style="color: rgb(180, 36, 25);">f"""</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(180, 36, 25);"> SELECT parent, child, 1 AS allow, 'root user' AS reason</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(180, 36, 25);"> FROM (</span><span style="color: rgb(180, 36, 25);">{resources_sql</span><span style="color: rgb(180, 36, 25);">})</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(180, 36, 25);"> """</span> | |
| <span style="color: rgb(242, 242, 242);"> rules.append(PermissionSQL(</span> | |
| <span style="color: rgb(242, 242, 242);"> source=</span><span style="color: rgb(180, 36, 25);">"root_permissions"</span><span style="color: rgb(242, 242, 242);">,</span> | |
| <span style="color: rgb(242, 242, 242);"> sql=sql,</span> | |
| <span style="color: rgb(242, 242, 242);"> params=</span><span style="color: rgb(242, 242, 242);">{</span><span style="color: rgb(242, 242, 242);">},</span> | |
| <span style="color: rgb(242, 242, 242);"> ))</span> | |
| <span style="color: rgb(242, 242, 242);"> Then let the function continue to line 224 to add config rules.</span> | |
| <span style="color: rgb(255, 255, 255); background: rgb(42, 42, 42);">> Add a test for this, run it and watch it fail, then fix the issue and run the test again and commit just the files you </span> | |
| <span style="color: rgb(255, 255, 255); background: rgb(42, 42, 42);">touched </span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Let me first check what permission endpoint tests look like:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Search</span><span style="color: rgb(242, 242, 242);">(pattern: "def test.*allowed.*root|test.*root.*permission", path: "tests/test_permission_endpoints.py", output_mode:</span> | |
| <span style="color: rgb(242, 242, 242);"> "content")</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Found </span><span style="color: rgb(255, 255, 255);">0 </span><span style="color: rgb(242, 242, 242);">lines </span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Read</span><span style="color: rgb(242, 242, 242);">(tests/test_permission_endpoints.py)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Read </span><span style="color: rgb(255, 255, 255);">80</span><span style="color: rgb(242, 242, 242);"> lines</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Search</span><span style="color: rgb(242, 242, 242);">(pattern: "def test.*allowed.*json|async def test_allowed", path: "tests/test_permission_endpoints.py", output_mode:</span> | |
| <span style="color: rgb(242, 242, 242);"> "content")</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Found </span><span style="color: rgb(255, 255, 255);">158 </span><span style="color: rgb(242, 242, 242);">lines </span><span style="color: rgb(97, 97, 97);">(ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Now let me add a test that reproduces issue #2509:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Read</span><span style="color: rgb(242, 242, 242);">(tests/test_permission_endpoints.py)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Read </span><span style="color: rgb(255, 255, 255);">50</span><span style="color: rgb(242, 242, 242);"> lines</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Let me find the end of the file to add the test:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Bash</span><span style="color: rgb(242, 242, 242);">(wc -l /Users/simon/Dropbox/Development/datasette/tests/test_permission_endpoints.py)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);"> 496 /Users/simon/Dropbox/Development/datasette/tests/test_permission_endpoints.py</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Read</span><span style="color: rgb(242, 242, 242);">(tests/test_permission_endpoints.py)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Read </span><span style="color: rgb(255, 255, 255);">18</span><span style="color: rgb(242, 242, 242);"> lines</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Now let me add a test at the end of the file that reproduces the issue:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Update</span><span style="color: rgb(242, 242, 242);">(tests/test_permission_endpoints.py)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Updated </span><span style="color: rgb(255, 255, 255);">tests/test_permission_endpoints.py</span><span style="color: rgb(242, 242, 242);"> with </span><span style="color: rgb(255, 255, 255);">38</span><span style="color: rgb(242, 242, 242);"> additions </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 494 </span><span style="color: rgb(255, 255, 255);"> # Check for HTML structure</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 495 </span><span style="color: rgb(255, 255, 255);"> text = response.text</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 496 </span><span style="color: rgb(255, 255, 255);"> assert "<!DOCTYPE html>" in text or "<html" in text</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 497 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 498 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 499 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ @pytest.mark.asyncio</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 500 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ async def test_root_user_respects_settings_deny():</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 501 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ """</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 502 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ Test for issue #2509: Settings-based deny rules should override root user privileges.</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 503 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 504 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ When a database has `allow: false` in settings, the root user should NOT see</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 505 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ that database in /-/allowed.json?action=view-database, even though root normally</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 506 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ has all permissions.</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 507 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ """</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 508 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ ds = Datasette(</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 509 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ config=</span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">{</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 510 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ "databases": </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">{</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 511 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ "content": </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">{</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 512 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ "allow": False, # Deny everyone, including root</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 513 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">}</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 514 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">}</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 515 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">}</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 516 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ )</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 517 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ ds.root_enabled = True</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 518 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ await ds.invoke_startup()</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 519 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ ds.add_memory_database("content")</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 520 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 521 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ # Root user should NOT see the content database because settings deny it</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 522 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ response = await ds.client.get(</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 523 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ "/-/allowed.json?action=view-database",</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 524 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ cookies=</span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">{"ds_actor": ds.client.actor_cookie(</span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">{"id": "root"</span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">})</span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">},</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 525 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ )</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 526 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ assert response.status_code == 200</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 527 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ data = response.json()</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 528 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 529 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ # Check that content database is NOT in the allowed list</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 530 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ allowed_databases = [item["parent"] for item in data["items"]]</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 531 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ assert "content" not in allowed_databases, (</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 532 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ f"Root user should not see 'content' database when settings deny it, "</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 533 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ f"but found it in: </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">{allowed_databases</span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">}"</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 534 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ )</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Now let's run the test and watch it fail:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Bash</span><span style="color: rgb(242, 242, 242);">(python3.12-venv/bin/pytest tests/test_permission_endpoints.py::test_root_user_respects_settings_deny -xvs)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(252, 81, 109);">Error: ============================= test session starts ==============================</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(252, 81, 109);">platform darwin -- Python 3.12.0, pytest-7.4.4, pluggy-1.4.0 -- </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(252, 81, 109);">/Users/simon/Dropbox/Development/datasette/python3.12-venv/bin/python3.12</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(252, 81, 109);">cachedir: .pytest_cache</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(252, 81, 109);">SQLite: 3.42.0</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(252, 81, 109);">rootdir: /Users/simon/Dropbox/Development/datasette</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(252, 81, 109);">configfile: pytest.ini</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(252, 81, 109);">plugins: timeout-2.2.0, asyncio-0.23.4, xdist-3.5.0, anyio-4.2.0</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(252, 81, 109);">asyncio: mode=Mode.STRICT</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(252, 81, 109);">collecting ... collected 1 item</span> | |
| <span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(97, 97, 97);"> +55 lines (</span><span style="color: rgb(102, 102, 102);">ctrl+o</span><span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);">to see all)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Perfect! The test fails exactly as expected. The root user is seeing the 'content' database even though settings deny</span> | |
| <span style="color: rgb(242, 242, 242);"> it. Now let's fix the bug:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Now let's fix the bug in default_permissions.py:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Update</span><span style="color: rgb(242, 242, 242);">(datasette/default_permissions.py)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">Updated </span><span style="color: rgb(255, 255, 255);">datasette/default_permissions.py</span><span style="color: rgb(242, 242, 242);"> with </span><span style="color: rgb(255, 255, 255);">11</span><span style="color: rgb(242, 242, 242);"> additions and </span><span style="color: rgb(255, 255, 255);">8</span><span style="color: rgb(242, 242, 242);"> removals </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 203 </span><span style="color: rgb(255, 255, 255);"> </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 204 </span><span style="color: rgb(255, 255, 255);"> @hookimpl</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 205 </span><span style="color: rgb(255, 255, 255);"> async def permission_resources_sql(datasette, actor, action):</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 206 </span><span style="color: rgb(255, 255, 255); background: rgb(101, 28, 41);">- # Root user with root_enabled gets all permissions</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 206 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ rules: list[PermissionSQL] = []</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 207 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 208 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ # Root user with root_enabled gets all permissions at global level</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 209 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ # But config rules can still override at more specific levels (database/table)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 210 </span><span style="color: rgb(255, 255, 255);"> if datasette.root_enabled and actor and actor.get("id") == "root":</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 211 </span><span style="color: rgb(255, 255, 255); background: rgb(101, 28, 41);">- # Return SQL that grants access to ALL resources for this action</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 211 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ # Add a global-level allow rule for root</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 212 </span><span style="color: rgb(255, 255, 255);"> action_obj = datasette.actions.get(action)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 213 </span><span style="color: rgb(255, 255, 255);"> if action_obj and action_obj.resource_class:</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 214 </span><span style="color: rgb(255, 255, 255);"> resources_sql = action_obj.resource_class.resources_sql()</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);">...</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 213 </span><span style="color: rgb(255, 255, 255);"> SELECT parent, child, 1 AS allow, 'root user' AS reason</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 214 </span><span style="color: rgb(255, 255, 255);"> FROM (</span><span style="color: rgb(255, 255, 255);">{resources_sql</span><span style="color: rgb(255, 255, 255);">})</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 215 </span><span style="color: rgb(255, 255, 255);"> """</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 216 </span><span style="color: rgb(255, 255, 255); background: rgb(101, 28, 41);">- return PermissionSQL(</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 217 </span><span style="color: rgb(255, 255, 255); background: rgb(101, 28, 41);">- source="root_permissions",</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 218 </span><span style="color: rgb(255, 255, 255); background: rgb(101, 28, 41);">- sql=sql,</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 219 </span><span style="color: rgb(255, 255, 255); background: rgb(101, 28, 41);">- params=</span><span style="color: rgb(255, 255, 255); background: rgb(101, 28, 41);">{</span><span style="color: rgb(255, 255, 255); background: rgb(101, 28, 41);">},</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 216 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ rules.append(</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 217 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ PermissionSQL(</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 218 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ source="root_permissions",</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 219 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ sql=sql,</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 220 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ params=</span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">{</span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">},</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 221 </span><span style="color: rgb(255, 255, 255); background: rgb(28, 75, 32);">+ )</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 222 </span><span style="color: rgb(255, 255, 255);"> )</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 223 </span><span style="color: rgb(255, 255, 255);"> </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 224 </span><span style="color: rgb(255, 255, 255); background: rgb(101, 28, 41);">- rules: list[PermissionSQL] = []</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 225 </span><span style="color: rgb(255, 255, 255); background: rgb(101, 28, 41);">- </span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 224 </span><span style="color: rgb(255, 255, 255);"> config_rules = await _config_permission_rules(datasette, actor, action)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 225 </span><span style="color: rgb(255, 255, 255);"> rules.extend(config_rules)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);"> 226 </span><span style="color: rgb(255, 255, 255);"> </span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Wait, I realize there's an issue with my fix. The root user rule is providing rules for ALL resources (databases,</span> | |
| <span style="color: rgb(242, 242, 242);"> tables), which would be at child or parent level, not global level. The issue is more subtle. Let me re-read the SQL</span> | |
| <span style="color: rgb(242, 242, 242);"> being generated:</span> | |
| <span style="color: rgb(242, 242, 242);"> The root user SQL is:</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">SELECT</span><span style="color: rgb(242, 242, 242);"> parent, child, </span><span style="color: rgb(47, 180, 29);">1</span><span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">AS</span><span style="color: rgb(242, 242, 242);"> allow, </span><span style="color: rgb(180, 36, 25);">'root user'</span><span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">AS</span><span style="color: rgb(242, 242, 242);"> reason</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">FROM</span><span style="color: rgb(242, 242, 242);"> (</span><span style="color: rgb(64, 11, 217);">SELECT</span><span style="color: rgb(242, 242, 242);"> database_name </span><span style="color: rgb(64, 11, 217);">AS</span><span style="color: rgb(242, 242, 242);"> parent, </span><span style="color: rgb(64, 11, 217);">NULL</span><span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">AS</span><span style="color: rgb(242, 242, 242);"> child </span><span style="color: rgb(64, 11, 217);">FROM</span><span style="color: rgb(242, 242, 242);"> catalog_databases)</span> | |
| <span style="color: rgb(242, 242, 242);"> This returns rows like </span><span style="color: rgb(162, 168, 247);">('content', NULL, 1, 'root user')</span><span style="color: rgb(242, 242, 242);"> - which is a </span><span style="color: rgb(255, 255, 255);">parent-level</span><span style="color: rgb(242, 242, 242);"> rule (depth=1).</span> | |
| <span style="color: rgb(242, 242, 242);"> The config deny rule for </span><span style="color: rgb(162, 168, 247);">databases.content.allow: false</span><span style="color: rgb(242, 242, 242);"> is:</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">SELECT</span><span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(180, 36, 25);">'content'</span><span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">AS</span><span style="color: rgb(242, 242, 242);"> parent, </span><span style="color: rgb(64, 11, 217);">NULL</span><span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">AS</span><span style="color: rgb(242, 242, 242);"> child, </span><span style="color: rgb(47, 180, 29);">0</span><span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">AS</span><span style="color: rgb(242, 242, 242);"> allow, </span><span style="color: rgb(180, 36, 25);">'config deny...'</span><span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(64, 11, 217);">AS</span><span style="color: rgb(242, 242, 242);"> reason</span> | |
| <span style="color: rgb(242, 242, 242);"> This is also a </span><span style="color: rgb(255, 255, 255);">parent-level</span><span style="color: rgb(242, 242, 242);"> rule (depth=1).</span> | |
| <span style="color: rgb(242, 242, 242);"> According to the cascading logic: "Within the same depth: deny (0) beats allow (1)"</span> | |
| <span style="color: rgb(242, 242, 242);"> So both rules are at depth=1, and deny should win! But the issue is that the root rule is returning EARLY and never</span> | |
| <span style="color: rgb(242, 242, 242);"> adding the config rules.</span> | |
| <span style="color: rgb(242, 242, 242);"> My fix should work - let me run the test:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Bash</span><span style="color: rgb(242, 242, 242);">(python3.12-venv/bin/pytest tests/test_permission_endpoints.py::test_root_user_respects_settings_deny -xvs)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> ============================= test session starts ============================== </span> | |
| <span style="color: rgb(242, 242, 242);"> platform darwin -- Python 3.12.0, pytest-7.4.4, pluggy-1.4.0 -- /Users/simon/Dropbox/Development/datasette/python3.1</span> | |
| <span style="color: rgb(242, 242, 242);"> 2-venv/bin/python3.12</span> | |
| <span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(97, 97, 97);"> +19 lines (ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Excellent! The test now passes. Let me run a few more tests to make sure I didn't break anything:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Bash</span><span style="color: rgb(242, 242, 242);">(python3.12-venv/bin/pytest tests/test_permission_endpoints.py -xvs 2>&1 | tail -30)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> tests/test_permission_endpoints.py::test_rules_json_basic[/-/rules.json?action=view-instance-200-expected_keys0] PAS</span> | |
| <span style="color: rgb(242, 242, 242);"> SED</span> | |
| <span style="color: rgb(242, 242, 242);"> tests/test_permission_endpoints.py::test_rules_json_basic[/-/rules.json?action=view-database-200-expected_keys1] PAS</span> | |
| <span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(97, 97, 97);"> +32 lines (ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Perfect! All 52 tests pass. Let me also run some general permission tests to make sure root still works correctly for</span> | |
| <span style="color: rgb(242, 242, 242);"> non-denied resources:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Bash</span><span style="color: rgb(242, 242, 242);">(python3.12-venv/bin/pytest tests/test_permissions.py -x -k "root" 2>&1 | tail -30)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">============================= test session starts ==============================</span> | |
| <span style="color: rgb(242, 242, 242);"> platform darwin -- Python 3.12.0, pytest-7.4.4, pluggy-1.4.0</span> | |
| <span style="color: rgb(242, 242, 242);"> SQLite: 3.42.0</span> | |
| <span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(97, 97, 97);"> +17 lines (ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Great! Now let's commit just the files we touched:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Bash</span><span style="color: rgb(242, 242, 242);">(git status --short)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);"> M datasette/default_permissions.py </span> | |
| <span style="color: rgb(242, 242, 242);"> M tests/test_permission_endpoints.py</span> | |
| <span style="color: rgb(242, 242, 242);"> ?? RELEASE_NOTES_DRAFT.md</span> | |
| <span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(97, 97, 97);"> +40 lines (ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Perfect! Only the two files we modified. Let me stage and commit them:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Bash</span><span style="color: rgb(242, 242, 242);">(git add datasette/default_permissions.py tests/test_permission_endpoints.py)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(97, 97, 97);">(No content)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Bash</span><span style="color: rgb(242, 242, 242);">(git diff --staged)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">diff --git a/datasette/default_permissions.py b/datasette/default_permissions.py</span> | |
| <span style="color: rgb(242, 242, 242);"> index e873361c..e2ef8ad0 100644</span> | |
| <span style="color: rgb(242, 242, 242);"> --- a/datasette/default_permissions.py</span> | |
| <span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(97, 97, 97);"> +83 lines (ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Perfect! The changes look good. Now let's commit:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Bash</span><span style="color: rgb(242, 242, 242);">(git commit -m "Fix #2509: Settings-based deny rules now override root user privileges</span> <span style="color: rgb(242, 242, 242);">)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">[resource-based-permissions f5d1fe95] Fix #2509: Settings-based deny rules now override root user privileges</span> | |
| <span style="color: rgb(242, 242, 242);"> 2 files changed, 49 insertions(+), 8 deletions(-)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Perfect! Let me verify the commit was created correctly:</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Bash</span><span style="color: rgb(242, 242, 242);">(git log --oneline -3)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">f5d1fe95 Fix #2509: Settings-based deny rules now override root user privileges </span> | |
| <span style="color: rgb(242, 242, 242);"> d1ea067f Migrate homepage to use bulk allowed_resources() and fix NULL handling in SQL JOINs</span> | |
| <span style="color: rgb(242, 242, 242);"> 3adddad6 Add parent filter and include_is_private to allowed_resources()</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Bash</span><span style="color: rgb(242, 242, 242);">(git show --stat HEAD)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span>⎿<span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(242, 242, 242);">commit f5d1fe9587e807555e4d69ab4aa287a8ad8aea78</span> | |
| <span style="color: rgb(242, 242, 242);"> Author: Simon Willison <[email protected]></span> | |
| <span style="color: rgb(242, 242, 242);"> Date: Fri Oct 24 10:56:51 2025 -0700</span> | |
| <span style="color: rgb(242, 242, 242);"> </span> <span style="color: rgb(97, 97, 97);"> +21 lines (ctrl+o to expand)</span> | |
| ⏺<span style="color: rgb(242, 242, 242);"> Excellent! The fix has been successfully committed. Let me provide a summary:</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(255, 255, 255);">Summary</span> | |
| <span style="color: rgb(242, 242, 242);"> Successfully fixed issue #2509 where settings-based deny rules weren't being respected for the root user in the</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(162, 168, 247);">/-/allowed</span><span style="color: rgb(242, 242, 242);"> and </span><span style="color: rgb(162, 168, 247);">/-/rules</span><span style="color: rgb(242, 242, 242);"> endpoints.</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);">What I Did</span> | |
| <span style="color: rgb(242, 242, 242);"> 1. </span><span style="color: rgb(255, 255, 255);">Added a test</span><span style="color: rgb(242, 242, 242);"> (</span><span style="color: rgb(162, 168, 247);">test_root_user_respects_settings_deny</span><span style="color: rgb(242, 242, 242);">) that:</span> | |
| <span style="color: rgb(242, 242, 242);"> - Creates a Datasette instance with </span><span style="color: rgb(162, 168, 247);">databases.content.allow: false</span> | |
| <span style="color: rgb(242, 242, 242);"> - Enables root user with </span><span style="color: rgb(162, 168, 247);">root_enabled = True</span> | |
| <span style="color: rgb(242, 242, 242);"> - Verifies that the root user does NOT see the "content" database in </span><span style="color: rgb(162, 168, 247);">/-/allowed.json?action=view-database</span> | |
| <span style="color: rgb(242, 242, 242);"> 2. </span><span style="color: rgb(255, 255, 255);">Ran the test and verified it failed</span><span style="color: rgb(242, 242, 242);"> - The root user was incorrectly seeing the "content" database despite the deny</span> | |
| <span style="color: rgb(242, 242, 242);"> setting</span> | |
| <span style="color: rgb(242, 242, 242);"> 3. </span><span style="color: rgb(255, 255, 255);">Fixed the bug</span><span style="color: rgb(242, 242, 242);"> in </span><span style="color: rgb(162, 168, 247);">datasette/default_permissions.py</span><span style="color: rgb(242, 242, 242);">:</span> | |
| <span style="color: rgb(242, 242, 242);"> - Changed </span><span style="color: rgb(162, 168, 247);">permission_resources_sql()</span><span style="color: rgb(242, 242, 242);"> to </span><span style="color: rgb(255, 255, 255);">append</span><span style="color: rgb(242, 242, 242);"> root permissions to the rules list instead of returning early</span> | |
| <span style="color: rgb(242, 242, 242);"> - This allows config-based deny rules to be evaluated alongside root's allow rules</span> | |
| <span style="color: rgb(242, 242, 242);"> - The SQL cascading logic correctly applies: deny rules at the same depth beat allow rules</span> | |
| <span style="color: rgb(242, 242, 242);"> 4. </span><span style="color: rgb(255, 255, 255);">Verified the fix</span><span style="color: rgb(242, 242, 242);"> - Test now passes, and all 52 permission endpoint tests pass</span> | |
| <span style="color: rgb(242, 242, 242);"> 5. </span><span style="color: rgb(255, 255, 255);">Committed only the touched files</span><span style="color: rgb(242, 242, 242);">:</span> | |
| <span style="color: rgb(242, 242, 242);"> - </span><span style="color: rgb(162, 168, 247);">datasette/default_permissions.py</span><span style="color: rgb(242, 242, 242);"> (19 lines changed)</span> | |
| <span style="color: rgb(242, 242, 242);"> - </span><span style="color: rgb(162, 168, 247);">tests/test_permission_endpoints.py</span><span style="color: rgb(242, 242, 242);"> (38 lines added)</span> | |
| <span style="color: rgb(242, 242, 242);"> </span><span style="color: rgb(97, 97, 97);">Why The Fix Works</span> | |
| <span style="color: rgb(242, 242, 242);"> The bug was that root's blanket "allow all" rule was being returned immediately, preventing settings-based deny rules</span> | |
| <span style="color: rgb(242, 242, 242);"> from ever being added to the SQL query. Now:</span> | |
| <span style="color: rgb(242, 242, 242);"> - Root gets permission rules at the resource level (e.g., </span><span style="color: rgb(162, 168, 247);">parent='content', child=NULL</span><span style="color: rgb(242, 242, 242);"> for databases)</span> | |
| <span style="color: rgb(242, 242, 242);"> - Settings provide deny rules at the same level (e.g., </span><span style="color: rgb(162, 168, 247);">parent='content', child=NULL, allow=0</span><span style="color: rgb(242, 242, 242);">)</span> | |
| <span style="color: rgb(242, 242, 242);"> - The SQL cascading logic says: </span><span style="color: rgb(255, 255, 255);">at the same depth, deny beats allow</span> | |
| <span style="color: rgb(242, 242, 242);"> - So the settings deny wins, correctly blocking access even for root</span> | |
| </pre> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment