From 44b5c175fcb5676aaff4402d06c6fc04c3926e0e Mon Sep 17 00:00:00 2001 From: Eike Date: Tue, 21 Jan 2025 18:01:33 +0100 Subject: [PATCH 1/2] chore: use basedpyright in nix dev setup --- flake.nix | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index f098e6f67..09259ddcd 100644 --- a/flake.nix +++ b/flake.nix @@ -63,14 +63,21 @@ AUTHZ_DB_KEY = "dev"; AUTHZ_DB_NO_TLS_CONNECTION = "true"; AUTHZ_DB_GRPC_PORT = "50051"; - ALEMBIC_CONFIG = "./components/renku_data_services/migrations/alembic.ini"; - NB_SERVER_OPTIONS__DEFAULTS_PATH = "server_defaults.json"; - NB_SERVER_OPTIONS__UI_CHOICES_PATH = "server_options.json"; LD_LIBRARY_PATH = "${pkgs.stdenv.cc.cc.lib}/lib"; POETRY_VIRTUALENVS_PREFER_ACTIVE_PYTHON = "true"; POETRY_VIRTUALENVS_OPTIONS_SYSTEM_SITE_PACKAGES = "true"; POETRY_INSTALLER_NO_BINARY = "ruff"; + ZED_ENDPOINT = "localhost:50051"; + ZED_TOKEN = "dev"; + + shellHook = '' + export FLAKE_ROOT="$(git rev-parse --show-toplevel)" + export PATH="$FLAKE_ROOT/.venv/bin:$PATH" + export ALEMBIC_CONFIG="$FLAKE_ROOT/components/renku_data_services/migrations/alembic.ini" + export NB_SERVER_OPTIONS__DEFAULTS_PATH="$FLAKE_ROOT/server_defaults.json" + export NB_SERVER_OPTIONS__UI_CHOICES_PATH="$FLAKE_ROOT/server_options.json" + ''; }; commonPackages = with pkgs; [ @@ -86,7 +93,7 @@ ruff-lsp poetry python312 - pyright + basedpyright rclone ]; in { From 84281e50eb0f5198e254d14275d585fc5b7a9ef1 Mon Sep 17 00:00:00 2001 From: Eike Date: Fri, 17 Jan 2025 14:16:39 +0100 Subject: [PATCH 2/2] feat: Adding new relation for listing private-only entities --- .../renku_data_services/authz/models.py | 1 + .../renku_data_services/authz/schemas.py | 74 +++++++++++++++++++ ...9854e7ea77_add_non_public_read_relation.py | 36 +++++++++ .../authz/test_authorization.py | 63 ++++++++++++++++ .../renku_data_services/authz/test_schemas.py | 49 ++++++++++++ 5 files changed, 223 insertions(+) create mode 100644 components/renku_data_services/migrations/versions/239854e7ea77_add_non_public_read_relation.py diff --git a/components/renku_data_services/authz/models.py b/components/renku_data_services/authz/models.py index 5cf4b43c3..4cb05fbf5 100644 --- a/components/renku_data_services/authz/models.py +++ b/components/renku_data_services/authz/models.py @@ -56,6 +56,7 @@ class Scope(Enum): READ_CHILDREN = "read_children" ADD_LINK = "add_link" IS_ADMIN = "is_admin" + NON_PUBLIC_READ = "non_public_read" @dataclass diff --git a/components/renku_data_services/authz/schemas.py b/components/renku_data_services/authz/schemas.py index 93470c855..33d8d119f 100644 --- a/components/renku_data_services/authz/schemas.py +++ b/components/renku_data_services/authz/schemas.py @@ -440,3 +440,77 @@ def generate_v4(public_project_ids: Iterable[str]) -> AuthzSchemaMigration: ) return AuthzSchemaMigration(up=up, down=down) + + +_v5: str = """\ +definition user {} + +definition group { + relation group_platform: platform + relation owner: user + relation editor: user + relation viewer: user + relation public_viewer: user:* | anonymous_user:* + permission read = public_viewer + read_children + permission read_children = viewer + write + permission write = editor + delete + permission change_membership = delete + permission delete = owner + group_platform->is_admin + permission non_public_read = owner + editor + viewer - public_viewer +} + +definition user_namespace { + relation user_namespace_platform: platform + relation owner: user + relation public_viewer: user:* | anonymous_user:* + permission read = public_viewer + read_children + permission read_children = delete + permission write = delete + permission delete = owner + user_namespace_platform->is_admin + permission non_public_read = owner - public_viewer +} + +definition anonymous_user {} + +definition platform { + relation admin: user + permission is_admin = admin +} + +definition project { + relation project_platform: platform + relation project_namespace: user_namespace | group + relation owner: user + relation editor: user + relation viewer: user + relation public_viewer: user:* | anonymous_user:* + permission read = public_viewer + viewer + write + project_namespace->read_children + permission read_linked_resources = viewer + editor + owner + project_platform->is_admin + permission write = editor + delete + project_namespace->write + permission change_membership = delete + permission delete = owner + project_platform->is_admin + project_namespace->delete + permission non_public_read = owner + editor + viewer + project_namespace->read_children - public_viewer +} + +definition data_connector { + relation data_connector_platform: platform + relation data_connector_namespace: user_namespace | group + relation linked_to: project + relation owner: user + relation editor: user + relation viewer: user + relation public_viewer: user:* | anonymous_user:* + permission read = public_viewer + viewer + write + \ + data_connector_namespace->read_children + read_from_linked_resource + permission read_from_linked_resource = linked_to->read_linked_resources + permission write = editor + delete + data_connector_namespace->write + permission change_membership = delete + permission delete = owner + data_connector_platform->is_admin + data_connector_namespace->delete + permission add_link = write + public_viewer + permission non_public_read = owner + editor + viewer + data_connector_namespace->read_children - public_viewer +}""" + +v5 = AuthzSchemaMigration( + up=[WriteSchemaRequest(schema=_v5)], + down=[WriteSchemaRequest(schema=_v4)], +) diff --git a/components/renku_data_services/migrations/versions/239854e7ea77_add_non_public_read_relation.py b/components/renku_data_services/migrations/versions/239854e7ea77_add_non_public_read_relation.py new file mode 100644 index 000000000..73ab78e24 --- /dev/null +++ b/components/renku_data_services/migrations/versions/239854e7ea77_add_non_public_read_relation.py @@ -0,0 +1,36 @@ +"""Add non-public-read relation + +Revision ID: 239854e7ea77 +Revises: d71f0f795d30 +Create Date: 2025-01-17 14:34:47.305393 + +""" + +import logging + +from renku_data_services.authz.config import AuthzConfig +from renku_data_services.authz.schemas import v5 + +# revision identifiers, used by Alembic. +revision = "239854e7ea77" +down_revision = "d71f0f795d30" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + config = AuthzConfig.from_env() + client = config.authz_client() + responses = v5.upgrade(client) + logging.info( + f"Finished upgrading the Authz schema to version 5 in Alembic revision {revision}, response: {responses}" + ) + + +def downgrade() -> None: + config = AuthzConfig.from_env() + client = config.authz_client() + responses = v5.downgrade(client) + logging.info( + f"Finished downgrading the Authz schema from version 5 in Alembic revision {revision}, response: {responses}" + ) diff --git a/test/components/renku_data_services/authz/test_authorization.py b/test/components/renku_data_services/authz/test_authorization.py index cfcfcede4..b48027236 100644 --- a/test/components/renku_data_services/authz/test_authorization.py +++ b/test/components/renku_data_services/authz/test_authorization.py @@ -308,3 +308,66 @@ async def test_listing_projects_with_access(app_config_instance: Config, bootstr assert private_project_id1_str not in set( await authz.resources_with_permission(admin_user, admin_user.id, ResourceType.project, Scope.DELETE) ) + + +@pytest.mark.asyncio +async def test_listing_non_public_projects(app_config_instance: Config, bootstrap_admins) -> None: + authz = app_config_instance.authz + public_project_id = ULID() + private_project_id1 = ULID() + private_project_id2 = ULID() + + public_project_id_str = str(public_project_id) + private_project_id1_str = str(private_project_id1) + private_project_id2_str = str(private_project_id2) + + namespace = Namespace( + id=ULID(), + slug="ns-121", + kind=NamespaceKind.user, + created_by=str(regular_user1.id), + underlying_resource_id=ULID(), + ) + assert regular_user1.id + assert regular_user2.id + public_project = Project( + id=public_project_id, + name=public_project_id_str, + slug=public_project_id_str, + namespace=namespace, + visibility=Visibility.PUBLIC, + created_by=regular_user1.id, + ) + private_project1 = Project( + id=private_project_id1, + name=private_project_id1_str, + slug=private_project_id1_str, + namespace=namespace, + visibility=Visibility.PRIVATE, + created_by=regular_user1.id, + ) + private_project2 = Project( + id=private_project_id2, + name=private_project_id2_str, + slug=private_project_id2_str, + namespace=namespace, + visibility=Visibility.PRIVATE, + created_by=regular_user2.id, + ) + for p in [public_project, private_project1, private_project2]: + changes = authz._add_project(p) + await authz.client.WriteRelationships(changes.apply) + + ids_user1 = await authz.resources_with_permission( + admin_user, regular_user1.id, ResourceType.project, Scope.NON_PUBLIC_READ + ) + ids_user2 = await authz.resources_with_permission( + admin_user, regular_user2.id, ResourceType.project, Scope.NON_PUBLIC_READ + ) + assert private_project_id1_str in set(ids_user1) + assert private_project_id2_str not in set(ids_user1) + assert public_project_id_str not in set(ids_user1) + + assert private_project_id2_str in set(ids_user2) + assert private_project_id1_str not in set(ids_user2) + assert public_project_id_str not in set(ids_user2) diff --git a/test/components/renku_data_services/authz/test_schemas.py b/test/components/renku_data_services/authz/test_schemas.py index 57a49b0c7..c2c938601 100644 --- a/test/components/renku_data_services/authz/test_schemas.py +++ b/test/components/renku_data_services/authz/test_schemas.py @@ -300,6 +300,49 @@ def v2_schema() -> SpiceDBSchema: ) +@pytest.fixture +def v5_schema() -> SpiceDBSchema: + return SpiceDBSchema( + schemas._v5, + relationships=[ + "project:p1#owner@user:u1", + "project:p1#public_viewer@user:*", + "project:p1#public_viewer@anonymous_user:*", + "project:p2#owner@user:u1", + "project:p3#editor@user:u2", + "project:p4#project_namespace@group:g1", + "group:g1#editor@user:u1", + "project:p5#viewer@user:u3", + "project:p6#owner@user:u4", + "project:p6#public_viewer@user:*", + "project:p6#public_viewer@anonymous_user:*", + ], + assertions={ + "assertTrue": [ + "project:p2#non_public_read@user:u1", + "project:p3#non_public_read@user:u2", + "project:p4#non_public_read@user:u1", + "project:p5#non_public_read@user:u3", + ], + "assertFalse": [ + "project:p1#non_public_read@user:u1", + "project:p1#non_public_read@user:u2", + "project:p1#non_public_read@user:u3", + "project:p2#non_public_read@user:u2", + "project:p2#non_public_read@user:u3", + "project:p3#non_public_read@user:u1", + "project:p3#non_public_read@user:u3", + "project:p4#non_public_read@user:u2", + "project:p4#non_public_read@user:u3", + "project:p5#non_public_read@user:u1", + "project:p5#non_public_read@user:u2", + "project:p6#non_public_read@user:u4", + ], + }, + validation={}, + ) + + def test_v1_schema(tmp_path: Path, v1_schema: SpiceDBSchema) -> None: validation_file = tmp_path / "validate.yaml" v1_schema.to_yaml(validation_file) @@ -310,3 +353,9 @@ def test_v2_schema(tmp_path: Path, v2_schema: SpiceDBSchema) -> None: validation_file = tmp_path / "validate.yaml" v2_schema.to_yaml(validation_file) check_call(["zed", "validate", validation_file.as_uri()]) + + +def test_v5_schema(tmp_path: Path, v5_schema: SpiceDBSchema) -> None: + validation_file = tmp_path / "validate.yaml" + v5_schema.to_yaml(validation_file) + check_call(["zed", "validate", validation_file.as_uri()])