Skip to content

Commit

Permalink
Merge pull request #19 from bento-platform/feat/cli/grant-modification
Browse files Browse the repository at this point in the history
feat(cli): add grant permission-adding/setting commands
  • Loading branch information
davidlougheed authored Dec 21, 2023
2 parents 0d6406d + 91ffe3a commit 9523f02
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 38 deletions.
66 changes: 56 additions & 10 deletions bento_authorization_service/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async def get_group(db: Database, id_: int) -> int:
print(json_model_dump_kwargs(g, sort_keys=True, indent=2))
return 0

print("No group found with that ID.", file=sys.stderr)
print(f"No group found with ID: {id_}", file=sys.stderr)
return 1


Expand Down Expand Up @@ -172,6 +172,32 @@ async def assign_all_cmd(_config: Config, db: Database, args) -> int:
return 1


async def add_grant_permissions_cmd(_config: Config, db: Database, args) -> int:
id_ = getattr(args, "grant_id", -1)
if (g := await db.get_grant(id_)) is not None:
if overlap := (ps := frozenset(args.permissions)).intersection(g.permissions):
print(f"Grant {id_} already has permissions {{{', '.join(overlap)}}}", file=sys.stderr)
else:
await db.add_grant_permissions(id_, ps)
return 0

print(f"No grant found with ID: {id_}", file=sys.stderr)
return 1


async def set_grant_permissions_cmd(_config: Config, db: Database, args) -> int:
id_ = getattr(args, "grant_id", -1)
if (await db.get_grant(id_)) is not None:
await db.set_grant_permissions(id_, frozenset(args.permissions))
return 0

print(f"No grant found with ID: {id_}", file=sys.stderr)
return 1


ENTITY_KWARGS = dict(type=str, help="The type of entity to list.")


async def main(args: list[str] | None, db: Database | None = None) -> int:
cfg = get_config()
args = args if args is not None else sys.argv[1:]
Expand All @@ -183,16 +209,20 @@ async def main(args: list[str] | None, db: Database | None = None) -> int:

subparsers = parser.add_subparsers()

entity_kwargs = dict(type=str, help="The type of entity to list.")

# list -------------------------------------------------------------------------------------------------------------
l_sub = subparsers.add_parser("list")
l_sub.set_defaults(func=list_cmd)
l_sub.add_argument("entity", choices=("permissions", "grants", "groups"), **entity_kwargs)
l_sub.add_argument("entity", choices=("permissions", "grants", "groups"), **ENTITY_KWARGS)
# ------------------------------------------------------------------------------------------------------------------

# get --------------------------------------------------------------------------------------------------------------
g_sub = subparsers.add_parser("get")
g_sub.set_defaults(func=get_cmd)
g_sub.add_argument("entity", choices=GET_DELETE_ENTITIES, **entity_kwargs)
g_sub.add_argument("entity", choices=GET_DELETE_ENTITIES, **ENTITY_KWARGS)
g_sub.add_argument("id", type=int, help="Entity ID")
# ------------------------------------------------------------------------------------------------------------------

# create -----------------------------------------------------------------------------------------------------------

c = subparsers.add_parser("create")
c_subparsers = c.add_subparsers()
Expand All @@ -210,18 +240,34 @@ async def main(args: list[str] | None, db: Database | None = None) -> int:
cr.add_argument("membership", type=str, help="JSON representation of the group membership.")
cr.add_argument("--notes", type=str, default="", help="Optional human-readable notes to add to the group.")

# ------------------------------------------------------------------------------------------------------------------

# delete -----------------------------------------------------------------------------------------------------------
d_sub = subparsers.add_parser("delete")
d_sub.set_defaults(func=delete_cmd)
d_sub.add_argument("entity", choices=GET_DELETE_ENTITIES, **entity_kwargs)
d_sub.add_argument("entity", choices=GET_DELETE_ENTITIES, **ENTITY_KWARGS)
d_sub.add_argument("id", type=int, help="Entity ID")
# ------------------------------------------------------------------------------------------------------------------

s_sub = subparsers.add_parser(
au_sub = subparsers.add_parser(
"assign-all-to-user",
help='Assigns all extant permissions for {"everything": true} to an issuer + subject combination.',
)
s_sub.set_defaults(func=assign_all_cmd)
s_sub.add_argument("iss", type=str, help="Issuer")
s_sub.add_argument("sub", type=str, help="Subject ID")
au_sub.set_defaults(func=assign_all_cmd)
au_sub.add_argument("iss", type=str, help="Issuer")
au_sub.add_argument("sub", type=str, help="Subject ID")

ap_sub = subparsers.add_parser("add-grant-permissions", help="Adds permission(s) to an existing grant.")
ap_sub.set_defaults(func=add_grant_permissions_cmd)
ap_sub.add_argument("grant_id", type=int, help="Grant ID (use `bento_authz list` to see grants)")
ap_sub.add_argument("permissions", type=str, nargs="+", help="Permissions")

sp_sub = subparsers.add_parser("set-grant-permissions", help="Edits a grant to have a new set of permissions.")
sp_sub.set_defaults(func=set_grant_permissions_cmd)
sp_sub.add_argument("grant_id", type=int, help="Grant ID (use `bento_authz list` to see grants)")
sp_sub.add_argument("permissions", type=str, nargs="+", help="Permissions")

# ------------------------------------------------------------------------------------------------------------------

p_args = parser.parse_args(args)
if not getattr(p_args, "func", None):
Expand Down
17 changes: 17 additions & 0 deletions bento_authorization_service/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,23 @@ async def create_grant(self, grant: GrantModel) -> int | None:

return res

async def add_grant_permissions(
self, grant_id: int, permissions: frozenset[str], existing_conn: asyncpg.Connection | None = None
) -> None:
conn: asyncpg.Connection
async with self.connect(existing_conn) as conn:
await conn.executemany(
'INSERT INTO grant_permissions ("grant", "permission") VALUES ($1, $2) ON CONFLICT DO NOTHING',
[(grant_id, p) for p in permissions],
)

async def set_grant_permissions(self, grant_id: int, permissions: frozenset[str]) -> None:
conn: asyncpg.Connection
async with self.connect() as conn:
async with conn.transaction():
await conn.execute('DELETE FROM grant_permissions WHERE "grant" = $1', grant_id)
await self.add_grant_permissions(grant_id, permissions, existing_conn=conn)

async def delete_grant(self, grant_id: int) -> None:
conn: asyncpg.Connection
async with self.connect() as conn:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "bento-authorization-service"
version = "0.5.1"
version = "0.6.0"
description = "Permissions and authorization service for the Bento platform."
authors = ["David Lougheed <[email protected]>"]
license = "LGPL-3.0-only"
Expand Down
26 changes: 0 additions & 26 deletions tests/test_base_db.py

This file was deleted.

37 changes: 36 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from bento_lib.auth.permissions import PERMISSIONS
from bento_lib.auth.permissions import PERMISSIONS, P_QUERY_DATA, P_INGEST_DATA

from bento_authorization_service import cli
from bento_authorization_service.config import get_config
Expand Down Expand Up @@ -163,6 +163,41 @@ async def test_cli_get_grant(capsys, db: Database, db_cleanup):
assert r == 1


# noinspection PyUnusedLocal
@pytest.mark.asyncio
async def test_cli_add_grant_permissions(capsys, db: Database, db_cleanup):
existing_grant = (await db.get_grants())[0] # default: view:permissions, edit:permissions

r = await cli.main(["add-grant-permissions", str(existing_grant.id), str(P_QUERY_DATA), str(P_INGEST_DATA)])
assert r == 0

assert (await db.get_grant(existing_grant.id)).permissions == existing_grant.permissions.union(
frozenset({P_QUERY_DATA, P_INGEST_DATA})
)

r = await cli.main(["add-grant-permissions", str(existing_grant.id), str(P_QUERY_DATA), str(P_INGEST_DATA)])
assert r == 0
captured = capsys.readouterr()
assert captured.err.startswith(f"Grant {existing_grant.id} already has permissions")

r = await cli.main(["add-grant-permissions", "0", str(P_QUERY_DATA), str(P_INGEST_DATA)])
assert r == 1


# noinspection PyUnusedLocal
@pytest.mark.asyncio
async def test_cli_set_grant_permissions(capsys, db: Database, db_cleanup):
existing_grant = (await db.get_grants())[0] # default: view:permissions, edit:permissions

r = await cli.main(["set-grant-permissions", str(existing_grant.id), str(P_QUERY_DATA), str(P_INGEST_DATA)])
assert r == 0

assert (await db.get_grant(existing_grant.id)).permissions == frozenset({P_QUERY_DATA, P_INGEST_DATA})

r = await cli.main(["set-grant-permissions", "0", str(P_QUERY_DATA), str(P_INGEST_DATA)])
assert r == 1


# noinspection PyUnusedLocal
@pytest.mark.asyncio
async def test_cli_delete_bad_entity(db: Database, db_cleanup):
Expand Down
32 changes: 32 additions & 0 deletions tests/test_grants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import pytest

from bento_lib.auth import permissions
from fastapi import status
from fastapi.testclient import TestClient
from pydantic import ValidationError
Expand Down Expand Up @@ -62,6 +63,37 @@ def test_bad_grant_permission_length():
GrantModel(**{**sd.TEST_GRANT_DAVID_PROJECT_1_QUERY_DATA.model_dump(), "permissions": frozenset()})


# noinspection PyUnusedLocal
@pytest.mark.asyncio
async def test_grant_create(db: Database, db_cleanup):
id_ = await db.create_grant(sd.TEST_GRANT_DAVID_PROJECT_1_QUERY_DATA)
assert (await db.get_grant(id_)) is not None


# noinspection PyUnusedLocal
@pytest.mark.asyncio
async def test_grant_add_permissions(db: Database, db_cleanup):
id_ = await db.create_grant(sd.TEST_GRANT_DAVID_PROJECT_1_QUERY_DATA)
assert (await db.get_grant(id_)).permissions == frozenset({permissions.P_QUERY_DATA})

await db.add_grant_permissions(id_, frozenset({permissions.P_EDIT_PERMISSIONS, permissions.P_INGEST_DATA}))
assert (await db.get_grant(id_)).permissions == frozenset(
{permissions.P_QUERY_DATA, permissions.P_EDIT_PERMISSIONS, permissions.P_INGEST_DATA}
)


# noinspection PyUnusedLocal
@pytest.mark.asyncio
async def test_grant_set_permissions(db: Database, db_cleanup):
id_ = await db.create_grant(sd.TEST_GRANT_DAVID_PROJECT_1_QUERY_DATA)
assert (await db.get_grant(id_)).permissions == frozenset({permissions.P_QUERY_DATA})

await db.set_grant_permissions(id_, frozenset({permissions.P_EDIT_PERMISSIONS, permissions.P_INGEST_DATA}))
assert (await db.get_grant(id_)).permissions == frozenset(
{permissions.P_EDIT_PERMISSIONS, permissions.P_INGEST_DATA}
)


# noinspection PyUnusedLocal
@pytest.mark.asyncio
async def test_grant_endpoints_create(test_client: TestClient, db: Database, db_cleanup):
Expand Down

0 comments on commit 9523f02

Please sign in to comment.