Skip to content

Commit

Permalink
Merge pull request #48 from bento-platform/feat/cli/public-data
Browse files Browse the repository at this point in the history
feat(cli): add command for 1st time setup of public data access
  • Loading branch information
davidlougheed authored Aug 30, 2024
2 parents 439aad8 + c5b17a0 commit 292661a
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 57 deletions.
112 changes: 84 additions & 28 deletions bento_authorization_service/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@
import sys
import types

from bento_lib.auth.permissions import PERMISSIONS, PERMISSIONS_BY_STRING
from typing import Any, Callable, Coroutine
from bento_lib.auth.permissions import (
PERMISSIONS,
PERMISSIONS_BY_STRING,
P_QUERY_PROJECT_LEVEL_BOOLEAN,
P_QUERY_DATASET_LEVEL_BOOLEAN,
P_QUERY_PROJECT_LEVEL_COUNTS,
P_QUERY_DATASET_LEVEL_COUNTS,
P_QUERY_DATA,
)
from typing import Any, Callable, Coroutine, Literal

from . import __version__
from .config import Config, get_config
from .db import Database, get_db
from .models import GrantModel, GroupModel, SubjectModel, ResourceModel, RESOURCE_EVERYTHING
from .models import GrantModel, GroupModel, SubjectModel, ResourceModel, RESOURCE_EVERYTHING, SUBJECT_EVERYONE
from .utils import json_model_dump_kwargs


Expand All @@ -20,6 +28,18 @@
GET_DELETE_ENTITIES = (ENTITIES.GRANT, ENTITIES.GROUP)


def grant_created_exit(g: int | None) -> Literal[0, 1]:
"""
Helper function to exit with a different message/error code depending on whether the grant was successfully created.
"""
if g is not None:
print(f"Grant successfully created: {g}")
return 0

print("Grant was not created.", file=sys.stderr)
return 1


def list_permissions():
for p in PERMISSIONS:
print(p)
Expand Down Expand Up @@ -50,23 +70,18 @@ async def list_cmd(_config: Config, db: Database, args):


async def create_grant(_config: Config, db: Database, args) -> int:
g = await db.create_grant(
GrantModel(
subject=SubjectModel.model_validate_json(getattr(args, "subject", "null")),
resource=ResourceModel.model_validate_json(getattr(args, "resource", "null")),
expiry=None, # TODO: support via flag
notes=getattr(args, "notes", ""),
permissions=frozenset(PERMISSIONS_BY_STRING[p] for p in args.permissions),
return grant_created_exit(
await db.create_grant(
GrantModel(
subject=SubjectModel.model_validate_json(getattr(args, "subject", "null")),
resource=ResourceModel.model_validate_json(getattr(args, "resource", "null")),
expiry=None, # TODO: support via flag
notes=getattr(args, "notes", ""),
permissions=frozenset(PERMISSIONS_BY_STRING[p] for p in args.permissions),
)
)
)

if g:
print(f"Grant successfully created: {g}")
return 0

print("Grant was not created.", file=sys.stderr)
return 1


async def create_group(_config: Config, db: Database, args) -> int:
g = await db.create_group(
Expand Down Expand Up @@ -154,22 +169,56 @@ async def delete_cmd(_config: Config, db: Database, args) -> int:


async def assign_all_cmd(_config: Config, db: Database, args) -> int:
g = await db.create_grant(
GrantModel(
subject=SubjectModel.model_validate({"iss": args.iss, "sub": args.sub}),
resource=RESOURCE_EVERYTHING,
permissions=PERMISSIONS,
expiry=None,
notes="Generated by the bento_authz CLI tool as a result of `bento_authz assign-all-to-user ...`",
return grant_created_exit(
await db.create_grant(
GrantModel(
subject=SubjectModel.model_validate({"iss": args.iss, "sub": args.sub}),
resource=RESOURCE_EVERYTHING,
permissions=PERMISSIONS,
expiry=None,
notes="Generated by the bento_authz CLI tool as a result of `bento_authz assign-all-to-user ...`",
)
)
)

if g:
print(f"Grant successfully created: {g}")

async def public_data_access_cmd(_config: Config, db: Database, args) -> int:
level: Literal["none", "bool", "counts", "full"] = args.level

if level == "full":
permissions = frozenset((P_QUERY_DATA,))
elif level == "counts":
permissions = frozenset((P_QUERY_PROJECT_LEVEL_COUNTS, P_QUERY_DATASET_LEVEL_COUNTS))
elif level == "bool": # boolean
permissions = frozenset((P_QUERY_PROJECT_LEVEL_BOOLEAN, P_QUERY_DATASET_LEVEL_BOOLEAN))
else: # none
print("Nothing to do; no access is the default state.")
return 0

print("Grant was not created.", file=sys.stderr)
return 1
if level == "full" and not args.force:
confirm = input(
"Are you sure you wish to give full data access permissions to everyone (even anonymous / signed-out "
"users?) [y/N]"
).lower()

if confirm not in ("y", "yes"):
print("Exiting without doing anything.")
return 0

return grant_created_exit(
await db.create_grant(
GrantModel(
subject=SUBJECT_EVERYONE,
resource=RESOURCE_EVERYTHING,
permissions=permissions,
expiry=None,
notes=(
f"Generated by the bento_authz CLI tool as a result of `bento_authz public-data-access {level} ..."
f"`"
),
)
)
)


async def add_grant_permissions_cmd(_config: Config, db: Database, args) -> int:
Expand Down Expand Up @@ -257,6 +306,13 @@ async def main(args: list[str] | None, db: Database | None = None) -> int:
au_sub.add_argument("iss", type=str, help="Issuer")
au_sub.add_argument("sub", type=str, help="Subject ID")

pd_sub = subparsers.add_parser(
"public-data-access", help="Assigns a data access permission level of choice for all data to anonymous users."
)
pd_sub.set_defaults(func=public_data_access_cmd)
pd_sub.add_argument("level", type=str, choices=("none", "bool", "counts", "full"), help="Data access level to give")
pd_sub.add_argument("--force", "-f", action="store_true")

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)")
Expand Down
16 changes: 8 additions & 8 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ aiodns = "^3.2.0"
aiofiles = "^24.1.0"
aiohttp = "^3.10.5"
asyncpg = "^0.29.0"
bento-lib = {extras = ["fastapi"], version = "^12.1.1"}
bento-lib = {extras = ["fastapi"], version = "^12.2.0"}
fastapi = {extras = ["all"], version = "^0.112.2"}
jsonschema = "^4.21.1"
pydantic = "^2.7.1"
Expand All @@ -30,7 +30,7 @@ pytest = "^8.2.1"
flake8 = "^7.0.0"
debugpy = "^1.8.0"
uvicorn = "^0.30.1"
pytest-asyncio = "^0.23.4"
pytest-asyncio = "^0.24.0"
httpx = "^0.27.0"
pytest-cov = "^5.0.0"
black = "^24.1.1"
Expand Down
34 changes: 26 additions & 8 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,28 @@ async def decode(self, token: str) -> dict:


async def get_test_db() -> AsyncGenerator[Database, None]:
db_instance = Database(get_config().database_uri)
r = await db_instance.initialize(pool_size=1) # Small pool size for testing
if r:
# if we're initializing for the first time in this test -> cleanup flow, bootstrap permissions for the "david"
# test user.
await bootstrap_meta_permissions_for_david(db_instance)
yield db_instance


async def get_test_db_no_bootstrap() -> AsyncGenerator[Database, None]:
# same as the above, but without the default permissions - useful for testing database pool initialization/closing,
# or grant creation starting from a fresh database.
db_instance = Database(get_config().database_uri)
await db_instance.initialize(pool_size=1) # Small pool size for testing
await bootstrap_meta_permissions_for_david(db_instance)
# try:
# app.state.db = db_instance
yield db_instance
# finally:
# await db_instance.close()


db_fixture = pytest_asyncio.fixture(get_test_db, name="db")
db_fixture_no_bootstrap = pytest_asyncio.fixture(get_test_db_no_bootstrap, name="db_no")


@pytest_asyncio.fixture
async def db_cleanup(db: Database):
yield
async def _clean_db(db: Database):
conn: asyncpg.Connection
async with db.connect() as conn:
await conn.execute("DROP TABLE IF EXISTS groups")
Expand All @@ -69,6 +75,18 @@ async def db_cleanup(db: Database):
await db.close()


@pytest_asyncio.fixture
async def db_cleanup(db: Database):
yield
await _clean_db(db)


@pytest_asyncio.fixture
async def db_cleanup_no(db_no: Database):
yield
await _clean_db(db_no)


@lru_cache()
def get_mock_idp_manager():
return MockIdPManager("", TEST_TOKEN_AUD, frozenset(TEST_DISABLED_TOKEN_SIGNING_ALGOS), True)
Expand Down
Loading

0 comments on commit 292661a

Please sign in to comment.