kopia lustrzana https://github.com/simonw/datasette
unauthenticated: true method plus allow block docs, closes #825
rodzic
70dd14876e
commit
7633b9ab24
|
@ -867,10 +867,11 @@ async def async_call_with_supported_arguments(fn, **kwargs):
|
|||
|
||||
|
||||
def actor_matches_allow(actor, allow):
|
||||
if actor is None:
|
||||
actor = {"anonymous": True}
|
||||
if actor is None and allow and allow.get("unauthenticated") is True:
|
||||
return True
|
||||
if allow is None:
|
||||
return True
|
||||
actor = actor or {}
|
||||
for key, values in allow.items():
|
||||
if values == "*" and key in actor:
|
||||
return True
|
||||
|
|
|
@ -64,6 +64,91 @@ An **action** is a string describing the action the actor would like to perfom.
|
|||
|
||||
A **resource** is the item the actor wishes to interact with - for example a specific database or table. Some actions, such as ``permissions-debug``, are not associated with a particular resource.
|
||||
|
||||
Datasette's built-in view permissions (``view-database``, ``view-table`` etc) default to *allow* - unless you :ref:`configure additional permission rules <authentication_permissions_metadata>` unauthenticated users will be allowed to access content.
|
||||
|
||||
Permissions with potentially harmful effects should default to *deny*. Plugin authors should account for this when designing new plugins - for example, the `datasette-upload-csvs <https://github.com/simonw/datasette-upload-csvs>`__ plugin defaults to deny so that installations don't accidentally allow unauthenticated users to create new tables by uploading a CSV file.
|
||||
|
||||
.. _authentication_permissions_allow:
|
||||
|
||||
Defining permissions with "allow" blocks
|
||||
----------------------------------------
|
||||
|
||||
The standard way to define permissions in Datasette is to use an ``"allow"`` block. This is a JSON document describing which actors are allowed to perfom a permission.
|
||||
|
||||
The most basic form of allow block is this:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"allow": {
|
||||
"id": "root"
|
||||
}
|
||||
}
|
||||
|
||||
This will match any actors with an ``"id"`` property of ``"root"`` - for example, an actor that looks like this:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"id": "root",
|
||||
"name": "Root User"
|
||||
}
|
||||
|
||||
Allow keys can provide a list of values. These will match any actor that has any of those values.
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"allow": {
|
||||
"id": ["simon", "cleopaws"]
|
||||
}
|
||||
}
|
||||
|
||||
This will match any actor with an ``"id"`` of either ``"simon"`` or ``"cleopaws"``.
|
||||
|
||||
Actors can have properties that feature a list of values. These will be matched against the list of values in an allow block. Consider the following actor:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"id": "simon",
|
||||
"roles": ["staff", "developer"]
|
||||
}
|
||||
|
||||
This allow block will provide access to any actor that has ``"developer"`` as one of their roles:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"allow": {
|
||||
"roles": ["developer"]
|
||||
}
|
||||
}
|
||||
|
||||
Note that "roles" is not a concept that is baked into Datasette - it's a convention that plugins can choose to implement and act on.
|
||||
|
||||
If you want to provide access to any actor with a value for a specific key, use ``"*"``. For example, to match any logged-in user specify the following:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"allow": {
|
||||
"id": "*"
|
||||
}
|
||||
}
|
||||
|
||||
You can specify that unauthenticated actors (from anynomous HTTP requests) should be allowed access using the special ``"unauthenticated": true`` key in an allow block:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"allow": {
|
||||
"unauthenticated": true
|
||||
}
|
||||
}
|
||||
|
||||
Allow keys act as an "or" mechanism. An actor will be able to execute the query if any of their JSON properties match any of the values in the corresponding lists in the ``allow`` block.
|
||||
|
||||
.. _authentication_permissions_metadata:
|
||||
|
||||
Configuring permissions in metadata.json
|
||||
|
@ -96,49 +181,6 @@ Here's how to restrict access to your entire Datasette instance to just the ``"i
|
|||
}
|
||||
}
|
||||
|
||||
To allow any of the actors with an ``id`` matching a specific list of values, use this:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"allow": {
|
||||
"id": ["simon", "cleopaws"]
|
||||
}
|
||||
}
|
||||
|
||||
This works for other keys as well. Imagine an actor that looks like this:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"id": "simon",
|
||||
"roles": ["staff", "developer"]
|
||||
}
|
||||
|
||||
You can provide access to any user that has "developer" as one of their roles like so:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"allow": {
|
||||
"roles": ["developer"]
|
||||
}
|
||||
}
|
||||
|
||||
Note that "roles" is not a concept that is baked into Datasette - it's a convention that plugins can choose to implement and act on.
|
||||
|
||||
If you want to provide access to any actor with a value for a specific key, use ``"*"``. For example, to spceify that a query can be accessed by any logged-in user use this:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"allow": {
|
||||
"id": "*"
|
||||
}
|
||||
}
|
||||
|
||||
These keys act as an "or" mechanism. A actor will be able to execute the query if any of their JSON properties match any of the values in the corresponding lists in the ``allow`` block.
|
||||
|
||||
.. _authentication_permissions_database:
|
||||
|
||||
Controlling access to specific databases
|
||||
|
@ -297,6 +339,8 @@ view-instance
|
|||
|
||||
Top level permission - Actor is allowed to view any pages within this instance, starting at https://latest.datasette.io/
|
||||
|
||||
Default *allow*.
|
||||
|
||||
.. _permissions_view_database:
|
||||
|
||||
view-database
|
||||
|
@ -307,6 +351,8 @@ Actor is allowed to view a database page, e.g. https://latest.datasette.io/fixtu
|
|||
``resource`` - string
|
||||
The name of the database
|
||||
|
||||
Default *allow*.
|
||||
|
||||
.. _permissions_view_database_download:
|
||||
|
||||
view-database-download
|
||||
|
@ -317,6 +363,8 @@ Actor is allowed to download a database, e.g. https://latest.datasette.io/fixtur
|
|||
``resource`` - string
|
||||
The name of the database
|
||||
|
||||
Default *allow*.
|
||||
|
||||
.. _permissions_view_table:
|
||||
|
||||
view-table
|
||||
|
@ -327,6 +375,8 @@ Actor is allowed to view a table (or view) page, e.g. https://latest.datasette.i
|
|||
``resource`` - tuple: (string, string)
|
||||
The name of the database, then the name of the table
|
||||
|
||||
Default *allow*.
|
||||
|
||||
.. _permissions_view_query:
|
||||
|
||||
view-query
|
||||
|
@ -337,6 +387,8 @@ Actor is allowed to view a :ref:`canned query <canned_queries>` page, e.g. https
|
|||
``resource`` - tuple: (string, string)
|
||||
The name of the database, then the name of the canned query
|
||||
|
||||
Default *allow*.
|
||||
|
||||
.. _permissions_execute_sql:
|
||||
|
||||
execute-sql
|
||||
|
@ -347,9 +399,13 @@ Actor is allowed to run arbitrary SQL queries against a specific database, e.g.
|
|||
``resource`` - string
|
||||
The name of the database
|
||||
|
||||
Default *allow*.
|
||||
|
||||
.. _permissions_permissions_debug:
|
||||
|
||||
permissions-debug
|
||||
-----------------
|
||||
|
||||
Actor is allowed to view the ``/-/permissions`` debug page.
|
||||
|
||||
Default *deny*.
|
|
@ -184,11 +184,16 @@ await .permission_allowed(actor, action, resource=None, default=False)
|
|||
``resource`` - string, optional
|
||||
The resource, e.g. the name of the table. Only some permissions apply to a resource.
|
||||
|
||||
Check if the given actor has permission to perform the given action on the given resource. This uses plugins that implement the :ref:`plugin_permission_allowed` plugin hook to decide if the action is allowed or not.
|
||||
``default`` - optional, True or False
|
||||
Should this permission check be default allow or default deny.
|
||||
|
||||
If none of the plugins express an opinion, the return value will be the ``default`` argument. This is deny, but you can pass ``default=True`` to default allow instead.
|
||||
Check if the given actor has :ref:`permission <authentication_permissions>` to perform the given action on the given resource.
|
||||
|
||||
See :ref:`permissions` for a full list of permissions included in Datasette core.
|
||||
Some permission checks are carried out against :ref:`rules defined in metadata.json <authentication_permissions_metadata>`, while other custom permissions may be decided by plugins that implement the :ref:`plugin_permission_allowed` plugin hook.
|
||||
|
||||
If neither ``metadata.json`` nor any of the plugins provide an answer to the permission query the ``default`` argument will be returned.
|
||||
|
||||
See :ref:`permissions` for a full list of permission actions included in Datasette core.
|
||||
|
||||
.. _datasette_get_database:
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from .fixtures import app_client
|
||||
from bs4 import BeautifulSoup as Soup
|
||||
|
||||
|
||||
def test_auth_token(app_client):
|
||||
|
@ -20,26 +19,3 @@ def test_actor_cookie(app_client):
|
|||
cookie = app_client.ds.sign({"id": "test"}, "actor")
|
||||
response = app_client.get("/", cookies={"ds_actor": cookie})
|
||||
assert {"id": "test"} == app_client.ds._last_request.scope["actor"]
|
||||
|
||||
|
||||
def test_permissions_debug(app_client):
|
||||
app_client.ds._permission_checks.clear()
|
||||
assert 403 == app_client.get("/-/permissions").status
|
||||
# With the cookie it should work
|
||||
cookie = app_client.ds.sign({"id": "root"}, "actor")
|
||||
response = app_client.get("/-/permissions", cookies={"ds_actor": cookie})
|
||||
# Should show one failure and one success
|
||||
soup = Soup(response.body, "html.parser")
|
||||
check_divs = soup.findAll("div", {"class": "check"})
|
||||
checks = [
|
||||
{
|
||||
"action": div.select_one(".check-action").text,
|
||||
"result": bool(div.select(".check-result-true")),
|
||||
"used_default": bool(div.select(".check-used-default")),
|
||||
}
|
||||
for div in check_divs
|
||||
]
|
||||
assert [
|
||||
{"action": "permissions-debug", "result": True, "used_default": False},
|
||||
{"action": "permissions-debug", "result": False, "used_default": True},
|
||||
] == checks
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from .fixtures import app_client, assert_permissions_checked, make_app_client
|
||||
from bs4 import BeautifulSoup as Soup
|
||||
import pytest
|
||||
|
||||
|
||||
|
@ -283,3 +284,39 @@ def test_permissions_checked(app_client, path, permissions):
|
|||
response = app_client.get(path)
|
||||
assert response.status in (200, 403)
|
||||
assert_permissions_checked(app_client.ds, permissions)
|
||||
|
||||
|
||||
def test_permissions_debug(app_client):
|
||||
app_client.ds._permission_checks.clear()
|
||||
assert 403 == app_client.get("/-/permissions").status
|
||||
# With the cookie it should work
|
||||
cookie = app_client.ds.sign({"id": "root"}, "actor")
|
||||
response = app_client.get("/-/permissions", cookies={"ds_actor": cookie})
|
||||
# Should show one failure and one success
|
||||
soup = Soup(response.body, "html.parser")
|
||||
check_divs = soup.findAll("div", {"class": "check"})
|
||||
checks = [
|
||||
{
|
||||
"action": div.select_one(".check-action").text,
|
||||
"result": bool(div.select(".check-result-true")),
|
||||
"used_default": bool(div.select(".check-used-default")),
|
||||
}
|
||||
for div in check_divs
|
||||
]
|
||||
assert [
|
||||
{"action": "permissions-debug", "result": True, "used_default": False},
|
||||
{"action": "permissions-debug", "result": False, "used_default": True},
|
||||
] == checks
|
||||
|
||||
|
||||
@pytest.mark.parametrize("allow,expected", [
|
||||
({"id": "root"}, 403),
|
||||
({"id": "root", "unauthenticated": True}, 200),
|
||||
])
|
||||
def test_allow_unauthenticated(allow, expected):
|
||||
with make_app_client(
|
||||
metadata={
|
||||
"allow": allow
|
||||
}
|
||||
) as client:
|
||||
assert expected == client.get("/").status
|
||||
|
|
|
@ -464,12 +464,16 @@ def test_multi_params(data, should_raise):
|
|||
@pytest.mark.parametrize(
|
||||
"actor,allow,expected",
|
||||
[
|
||||
({"id": "root"}, None, True),
|
||||
({"id": "root"}, {}, False),
|
||||
({"anonymous": True}, {"anonymous": True}, True),
|
||||
(None, None, True),
|
||||
(None, {}, False),
|
||||
(None, {"id": "root"}, False),
|
||||
({"id": "root"}, None, True),
|
||||
({"id": "root"}, {}, False),
|
||||
({"id": "simon", "staff": True}, {"staff": True}, True),
|
||||
({"id": "simon", "staff": False}, {"staff": True}, False),
|
||||
# Special case for "unauthenticated": true
|
||||
(None, {"unauthenticated": True}, True),
|
||||
(None, {"unauthenticated": False}, False),
|
||||
# Special "*" value for any key:
|
||||
({"id": "root"}, {"id": "*"}, True),
|
||||
({}, {"id": "*"}, False),
|
||||
|
|
Ładowanie…
Reference in New Issue