Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table/database that is private due to inherited permissions does not show padlock #1829

Closed
simonw opened this issue Oct 4, 2022 · 8 comments

Comments

@simonw
Copy link
Owner

simonw commented Oct 4, 2022

I noticed that a table page that is private because the database or instance is private, e.g. this one:

image

Is not displaying the padlock icon that indicates the table is not visible to the public.

image

Same issue for the database page too, which in this case is private due to view-instance.

@simonw
Copy link
Owner Author

simonw commented Oct 4, 2022

Here's the relevant code from the table page:

# Ensure user has permission to view this table
await self.ds.ensure_permissions(
request.actor,
[
("view-table", (database_name, table_name)),
("view-database", database_name),
"view-instance",
],
)
private = not await self.ds.permission_allowed(
None, "view-table", (database_name, table_name), default=True
)

Note how ensure_permissions() there takes the table, database and instance into account... but the private assignment (used to decide if the padlock should display or not) only considers the view-table check.

Here's the same code for the database page:

"private": not await self.ds.permission_allowed(
None, "view-database", database, default=True
),

And for canned query pages:

if canned_query:
# Respect canned query permissions
await self.ds.ensure_permissions(
request.actor,
[
("view-query", (database, canned_query)),
("view-database", database),
"view-instance",
],
)
private = not await self.ds.permission_allowed(
None, "view-query", (database, canned_query), default=True
)

@simonw
Copy link
Owner Author

simonw commented Oct 4, 2022

There's also a check_visibility() helper which I'm not using in these particular cases but which may be relevant. It's called like this:

for view_name in await db.view_names():
visible, private = await self.ds.check_visibility(
request.actor,
"view-table",
(database, view_name),
)
if visible:
views.append(
{
"name": view_name,
"private": private,
}
)

And is defined here:

datasette/datasette/app.py

Lines 694 to 710 in 4218c9c

async def check_visibility(self, actor, action, resource):
"""Returns (visible, private) - visible = can you see it, private = can others see it too"""
visible = await self.permission_allowed(
actor,
action,
resource=resource,
default=True,
)
if not visible:
return False, False
private = not await self.permission_allowed(
None,
action,
resource=resource,
default=True,
)
return visible, private

It's actually documented as a public method here: https://docs.datasette.io/en/stable/internals.html#await-check-visibility-actor-action-resource-none

This convenience method can be used to answer the question "should this item be considered private, in that it is visible to me but it is not visible to anonymous users?"

It returns a tuple of two booleans, (visible, private). visible indicates if the actor can see this resource. private will be True if an anonymous user would not be able to view the resource.

Note that this documented method cannot actually do the right thing - because it's not being given the multiple permissions that need to be checked in order to completely answer the question.

So I probably need to redesign that method a bit.

@simonw
Copy link
Owner Author

simonw commented Oct 13, 2022

I think check_visibility should be changed to optionally accept permissions= which is the same list of tuples that can be passed to ensure_permissions.

@simonw
Copy link
Owner Author

simonw commented Oct 14, 2022

Here's what I've got so far:

diff --git a/datasette/app.py b/datasette/app.py
index 5fa4955c..df9eae49 100644
--- a/datasette/app.py
+++ b/datasette/app.py
@@ -1,5 +1,5 @@
 import asyncio
-from typing import Sequence, Union, Tuple
+from typing import Sequence, Union, Tuple, Optional
 import asgi_csrf
 import collections
 import datetime
@@ -707,7 +707,7 @@ class Datasette:
 
         Raises datasette.Forbidden() if any of the checks fail
         """
-        assert actor is None or isinstance(actor, dict)
+        assert actor is None or isinstance(actor, dict), "actor must be None or a dict"
         for permission in permissions:
             if isinstance(permission, str):
                 action = permission
@@ -732,23 +732,34 @@ class Datasette:
                 else:
                     raise Forbidden(action)
 
-    async def check_visibility(self, actor, action, resource):
+    async def check_visibility(
+        self,
+        actor: dict,
+        action: Optional[str] = None,
+        resource: Optional[str] = None,
+        permissions: Optional[
+            Sequence[Union[Tuple[str, Union[str, Tuple[str, str]]], str]]
+        ] = None,
+    ):
         """Returns (visible, private) - visible = can you see it, private = can others see it too"""
-        visible = await self.permission_allowed(
-            actor,
-            action,
-            resource=resource,
-            default=True,
-        )
-        if not visible:
+        if permissions:
+            assert (
+                not action and not resource
+            ), "Can't use action= or resource= with permissions="
+        else:
+            permissions = [(action, resource)]
+        try:
+            await self.ensure_permissions(actor, permissions)
+        except Forbidden:
             return False, False
-        private = not await self.permission_allowed(
-            None,
-            action,
-            resource=resource,
-            default=True,
-        )
-        return visible, private
+        # User can see it, but can the anonymous user see it?
+        try:
+            await self.ensure_permissions(None, permissions)
+        except Forbidden:
+            # It's visible but private
+            return True, True
+        # It's visible to everyone
+        return True, False
 
     async def execute(
         self,
diff --git a/datasette/views/table.py b/datasette/views/table.py
index 60c092f9..f73b0957 100644
--- a/datasette/views/table.py
+++ b/datasette/views/table.py
@@ -28,7 +28,7 @@ from datasette.utils import (
     urlsafe_components,
     value_as_boolean,
 )
-from datasette.utils.asgi import BadRequest, NotFound
+from datasette.utils.asgi import BadRequest, Forbidden, NotFound
 from datasette.filters import Filters
 from .base import DataView, DatasetteError, ureg
 from .database import QueryView
@@ -213,18 +213,16 @@ class TableView(DataView):
             raise NotFound(f"Table not found: {table_name}")
 
         # Ensure user has permission to view this table
-        await self.ds.ensure_permissions(
+        visible, private = await self.ds.check_visibility(
             request.actor,
-            [
+            permissions=[
                 ("view-table", (database_name, table_name)),
                 ("view-database", database_name),
                 "view-instance",
             ],
         )
-
-        private = not await self.ds.permission_allowed(
-            None, "view-table", (database_name, table_name), default=True
-        )
+        if not visible:
+            raise Forbidden("You do not have permission to view this table")
 
         # Handle ?_filter_column and redirect, if present
         redirect_params = filters_should_redirect(request.args)

Still needs tests and a documentation update.

Also this fix is currently only applied on the table page - needs to be applied on database, row and query pages too.

@simonw
Copy link
Owner Author

simonw commented Oct 14, 2022

@simonw
Copy link
Owner Author

simonw commented Oct 24, 2022

Useful test: if you sign in as root to https://latest.datasette.io/_internal/columns/_internal,columns,database_name you can see there's no padlock icon on that page or on https://latest.datasette.io/_internal/columns - fixing this bug should fix that.

@simonw simonw closed this as completed in 78dad23 Oct 24, 2022
@simonw
Copy link
Owner Author

simonw commented Oct 24, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant