Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adding new relation for listing private-only entities #611

Merged
merged 2 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()])
Loading