Skip to content

Commit

Permalink
edit-schema-create-table and edit-schema-alter-table permissions (#46)
Browse files Browse the repository at this point in the history
* Rename existing test, refs #22
* edit-schema-create-table permission, refs #22
* edit-schema-alter-table permission, refs #22
  • Loading branch information
simonw authored Dec 23, 2023
1 parent 1e48732 commit 37aa37a
Show file tree
Hide file tree
Showing 4 changed files with 378 additions and 118 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,21 @@ By default only [the root actor](https://datasette.readthedocs.io/en/stable/auth

## Permissions

The `edit-schema` permission governs access. You can use permission plugins such as [datasette-permissions-sql](https://github.com/simonw/datasette-permissions-sql) to grant additional access to the write interface.
The `edit-schema` permission provides access to all functionality.

You can use permission plugins such as [datasette-permissions-sql](https://github.com/simonw/datasette-permissions-sql) to grant additional access to the write interface.

These permission checks will call the `permission_allowed()` plugin hook with three arguments:

- `action` will be the string `"edit-schema"`
- `actor` will be the currently authenticated actor - usually a dictionary
- `resource` will be the string name of the database

You can instead use more finely-grained permissions.

- `edit-schema-create-table` allows users to create a new table. The `resource` will be the name of the database.
- `edit-schema-alter-table` allows users to alter the schema of a table. The `resource` will be a tuple of `(database_name, table_name)`.

## Screenshot

![datasette-edit-schema interface](https://raw.githubusercontent.com/simonw/datasette-edit-schema/main/datasette-edit-schema.png)
Expand Down
43 changes: 34 additions & 9 deletions datasette_edit_schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ def permission_allowed(actor, action, resource):
@hookimpl
def table_actions(datasette, actor, database, table):
async def inner():
if not await datasette.permission_allowed(
actor, "edit-schema", resource=database, default=False
):
if not await can_alter_table(datasette, actor, database, table):
return []
return [
{
Expand All @@ -45,12 +43,36 @@ async def inner():
return inner


async def can_create_table(datasette, actor, database):
if await datasette.permission_allowed(
actor, "edit-schema", resource=database, default=False
):
return True
# Or maybe they have edit-schema-create-table
if await datasette.permission_allowed(
actor, "edit-schema-create-table", resource=database, default=False
):
return True
return False


async def can_alter_table(datasette, actor, database, table):
if await datasette.permission_allowed(
actor, "edit-schema", resource=database, default=False
):
return True
# Or maybe they have edit-schema-create-table
if await datasette.permission_allowed(
actor, "edit-schema-alter-table", resource=(database, table), default=False
):
return True
return False


@hookimpl
def database_actions(datasette, actor, database):
async def inner():
if not await datasette.permission_allowed(
actor, "edit-schema", resource=database, default=False
):
if not await can_create_table(datasette, actor, database):
return []
return [
{
Expand Down Expand Up @@ -173,9 +195,9 @@ def get_columns(conn):


async def edit_schema_create_table(request, datasette):
databases = get_databases(datasette)
database_name = request.url_vars["database"]
await check_permissions(datasette, request, database_name)
if not await can_create_table(datasette, request.actor, database_name):
raise Forbidden("Permission denied for edit-schema-create-table")
try:
db = datasette.get_database(database_name)
except KeyError:
Expand Down Expand Up @@ -251,7 +273,10 @@ async def edit_schema_table(request, datasette):
table = unquote_plus(request.url_vars["table"])
databases = get_databases(datasette)
database_name = request.url_vars["database"]
await check_permissions(datasette, request, database_name)

if not await can_alter_table(datasette, request.actor, database_name, table):
raise Forbidden("Permission denied for edit-schema-alter-table")

try:
database = [db for db in databases if db.name == database_name][0]
except IndexError:
Expand Down
155 changes: 155 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from dataclasses import dataclass
from datasette import hookimpl
from datasette.app import Datasette
from datasette.plugins import pm
import pytest
import sqlite_utils


@pytest.fixture
def db_and_path(tmpdir):
path = str(tmpdir / "data.db")
db = sqlite_utils.Database(path)
db["creatures"].insert_all(
[
{"name": "Cleo", "description": "A medium sized dog"},
{"name": "Siroco", "description": "A troublesome Kakapo"},
]
)
db["other_table"].insert({"foo": "bar"})
db["empty_table"].create({"id": int, "name": str}, pk="id")
# Tables for testing foreign key editing
db["museums"].insert_all(
[
{
"id": "moma",
"name": "Museum of Modern Art",
"city_id": "nyc",
},
{
"id": "tate",
"name": "Tate Modern",
"city_id": "london",
},
{
"id": "exploratorium",
"name": "Exploratorium",
"city_id": "sf",
},
{
"id": "cablecars",
"name": "Cable Car Museum",
"city_id": "sf",
},
],
pk="id",
)
db["cities"].insert_all(
[
{
"id": "nyc",
"name": "New York City",
},
{
"id": "london",
"name": "London",
},
{
"id": "sf",
"name": "San Francisco",
},
],
pk="id",
)
db["distractions"].insert_all(
[
{
"id": "nyc",
"name": "Nice Yummy Cake",
}
],
pk="id",
)
db["has_foreign_keys"].insert(
{
"id": 1,
"distraction_id": "nyc",
},
pk="id",
foreign_keys=(("distraction_id", "distractions"),),
)
db["has_indexes"].insert(
{
"id": 1,
"name": "Cleo",
"description": "A medium sized dog",
},
pk="id",
)
db["has_indexes"].create_index(["name"], index_name="name_index")
db["has_indexes"].create_index(
["name"], index_name="name_unique_index", unique=True
)

return db, path


@pytest.fixture
def db_path(db_and_path):
return db_and_path[1]


@pytest.fixture
def db(db_and_path):
return db_and_path[0]


@pytest.fixture
def ds(db_path):
return Datasette([db_path])


@dataclass
class Rule:
actor_id: str
action: str
database: str = None
resource: str = None


@pytest.fixture
def rule():
return Rule


@pytest.fixture
def permission_plugin():
class PermissionPlugin:
__name__ = "PermissionPlugin"

# Use hookimpl and method names to register hooks
@hookimpl
def permission_allowed(self, datasette, actor, action, resource):
if not actor:
return None
database_name = None
resource_name = None
if isinstance(resource, str):
database_name = resource
elif resource:
database_name, resource_name = resource
to_match = Rule(
actor_id=actor["id"],
action=action,
database=database_name,
resource=resource_name,
)
if to_match in getattr(datasette, "_rules_allow", []):
return True
elif to_match in getattr(datasette, "_rules_deny", []):
return False
return None

pm.register(PermissionPlugin(), name="undo_permission_plugin")
yield
pm.unregister(name="undo_permission_plugin")
Loading

0 comments on commit 37aa37a

Please sign in to comment.