Skip to content

Commit

Permalink
Merge pull request #611 from SwissDataScienceCenter/private-resource-…
Browse files Browse the repository at this point in the history
…permission

feat: Adding new relation for listing private-only entities
  • Loading branch information
eikek authored Jan 22, 2025
2 parents f6f13da + 84281e5 commit 99ff4e9
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 4 deletions.
1 change: 1 addition & 0 deletions components/renku_data_services/authz/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions components/renku_data_services/authz/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)],
)
Original file line number Diff line number Diff line change
@@ -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}"
)
15 changes: 11 additions & 4 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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; [
Expand All @@ -86,7 +93,7 @@
ruff-lsp
poetry
python312
pyright
basedpyright
rclone
];
in {
Expand Down
63 changes: 63 additions & 0 deletions test/components/renku_data_services/authz/test_authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
49 changes: 49 additions & 0 deletions test/components/renku_data_services/authz/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()])

0 comments on commit 99ff4e9

Please sign in to comment.