Skip to content

Commit

Permalink
feat: reading DC links requires read on the DCs
Browse files Browse the repository at this point in the history
  • Loading branch information
olevski committed Feb 11, 2025
1 parent 9b3c5d8 commit 9c50b09
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 5 deletions.
4 changes: 2 additions & 2 deletions components/renku_data_services/authz/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,8 +489,8 @@ def generate_v4(public_project_ids: Iterable[str]) -> AuthzSchemaMigration:
relation editor: user
relation viewer: user
relation public_viewer: user:* | anonymous_user:*
permission read = read_children
permission read_children = public_viewer + viewer + write + project_namespace->read_children
permission read = public_viewer + read_children
permission read_children = viewer + write + project_namespace->read_children
permission write = editor + delete + project_namespace->write
permission change_membership = delete
permission delete = owner + project_platform->is_admin + project_namespace->delete
Expand Down
31 changes: 29 additions & 2 deletions components/renku_data_services/data_connectors/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,9 +499,13 @@ async def get_links_to(
message=f"Project with id '{project_id}' does not exist or you do not have access to it."
)

allowed_dcs = await self.authz.resources_with_permission(user, user.id, ResourceType.data_connector, Scope.READ)

async with self.session_maker() as session:
stmt = select(schemas.DataConnectorToProjectLinkORM).where(
schemas.DataConnectorToProjectLinkORM.project_id == project_id
stmt = (
select(schemas.DataConnectorToProjectLinkORM)
.where(schemas.DataConnectorToProjectLinkORM.project_id == project_id)
.where(schemas.DataConnectorToProjectLinkORM.data_connector_id.in_(allowed_dcs))
)
result = await session.scalars(stmt)
links_orm = result.all()
Expand Down Expand Up @@ -586,6 +590,21 @@ async def copy_link(
session: AsyncSession | None = None,
) -> models.DataConnectorToProjectLink:
"""Create a new link from a given data connector link to a project."""
allowed_to_read_dc = await self.authz.has_permission(
user, ResourceType.data_connector, link.data_connector_id, Scope.READ
)
allowed_to_write_project = await self.authz.has_permission(
user, ResourceType.project, link.project_id, Scope.WRITE
)
if not allowed_to_read_dc:
raise errors.MissingResourceError(
message=f"The data connector with ID {link.data_connector_id} does not exist "
"or you do not have access to it"
)
if not allowed_to_write_project:
raise errors.MissingResourceError(
message=f"The project with ID {link.project_id} does not exist or you do not have access to it"
)
unsaved_link = models.UnsavedDataConnectorToProjectLink(
data_connector_id=link.data_connector_id, project_id=project_id
)
Expand Down Expand Up @@ -615,6 +634,14 @@ async def delete_link(
if link_orm is None:
return None

allowed_to_write_project = await self.authz.has_permission(
user, ResourceType.project, link_orm.project_id, Scope.WRITE
)
if not allowed_to_write_project:
raise errors.MissingResourceError(
message=f"The project with ID {link_orm.project_id} does not exist or you do not have access to it"
)

link = link_orm.dump()
await session.delete(link_orm)
return link
Expand Down
145 changes: 144 additions & 1 deletion test/bases/renku_data_services/data_api/test_data_connectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1366,7 +1366,7 @@ async def test_creating_dc_in_project(sanic_client, user_headers) -> None:
dc_namespace = "test1/prj1"
payload = {
"name": "dc1",
"namespace": "test1/prj1",
"namespace": dc_namespace,
"slug": "dc1",
"storage": {
"configuration": {"type": "s3", "endpoint": "http://s3.aws.com"},
Expand Down Expand Up @@ -1408,3 +1408,146 @@ async def test_creating_dc_in_project(sanic_client, user_headers) -> None:
assert response.status_code == 200, response.text
assert len(response.json) == 1
assert response.json[0]["namespace"] == dc_namespace


@pytest.mark.asyncio
async def test_users_cannot_see_private_data_connectors_in_project(
sanic_client,
member_1_headers,
member_2_user: UserInfo,
member_2_headers,
user_headers,
regular_user: UserInfo,
) -> None:
# Create a group i.e. /test1
group_slug = "test1"
payload = {
"name": group_slug,
"slug": group_slug,
"description": "Group 1 Description",
}
_, response = await sanic_client.post("/api/data/groups", headers=member_1_headers, json=payload)
assert response.status_code == 201, response.text

# Add member_2 as reader on the group
payload = [
{
"id": member_2_user.id,
"role": "viewer",
}
]
_, response = await sanic_client.patch(
f"/api/data/groups/{group_slug}/members", headers=member_1_headers, json=payload
)
assert response.status_code == 200, response.text

# Create a public project in the group /test1/prj1
payload = {
"name": "prj1",
"namespace": "test1",
"slug": "prj1",
"visibility": "public",
}
_, response = await sanic_client.post("/api/data/projects", headers=member_1_headers, json=payload)
assert response.status_code == 201, response.text
project_id = response.json["id"]

# Create a private data connector in the group
dc_namespace = "test1"
storage_config = {
"configuration": {"type": "s3", "endpoint": "http://s3.aws.com"},
"source_path": "giab",
"target_path": "giab",
}
payload = {
"name": "dc-private",
"namespace": dc_namespace,
"slug": "dc-private",
"storage": storage_config,
"visibility": "private",
}
_, response = await sanic_client.post("/api/data/data_connectors", headers=member_1_headers, json=payload)
assert response.status_code == 201, response.text
assert response.json["namespace"] == dc_namespace
group_dc_id = response.json["id"]

# Link the private data connector to the project
payload = {"project_id": project_id}
_, response = await sanic_client.post(
f"/api/data/data_connectors/{group_dc_id}/project_links", headers=member_1_headers, json=payload
)
assert response.status_code == 201, response.text

# Create a data connector in the project /test1/proj1/dc1
dc_namespace = "test1/prj1"
payload = {
"name": "dc1",
"namespace": dc_namespace,
"slug": "dc1",
"storage": storage_config,
}
_, response = await sanic_client.post("/api/data/data_connectors", headers=member_1_headers, json=payload)
assert response.status_code == 201, response.text
assert response.json["namespace"] == dc_namespace
project_dc_id = response.json["id"]

# Link the data connector to the project
payload = {"project_id": project_id}
_, response = await sanic_client.post(
f"/api/data/data_connectors/{project_dc_id}/project_links", headers=member_1_headers, json=payload
)
assert response.status_code == 201, response.text

# Ensure that member_1 and member_2 can see both data connectors and their links
for req_headers in [member_1_headers, member_2_headers]:
_, response = await sanic_client.get("/api/data/data_connectors", headers=req_headers)
assert response.status_code == 200, response.text
assert len(response.json) == 2
assert response.json[0]["id"] == project_dc_id
assert response.json[1]["id"] == group_dc_id
_, response = await sanic_client.get(
f"/api/data/projects/{project_id}/data_connector_links", headers=req_headers
)
assert len(response.json) == 2
assert response.json[0]["data_connector_id"] == group_dc_id
assert response.json[1]["data_connector_id"] == project_dc_id

# The project is public so user should see it
_, response = await sanic_client.get(f"/api/data/projects/{project_id}", headers=user_headers)
assert response.status_code == 200, response.text
# User is not part of the project and the data connector is private so they should not see any data connectors
_, response = await sanic_client.get("/api/data/data_connectors", headers=user_headers)
assert response.status_code == 200, response.text
assert len(response.json) == 0
_, response = await sanic_client.get(f"/api/data/projects/{project_id}/data_connector_links", headers=user_headers)
assert len(response.json) == 0

# Anonymous users should see the project but not any of the DCs or the links
_, response = await sanic_client.get(f"/api/data/projects/{project_id}")
assert response.status_code == 200, response.text
_, response = await sanic_client.get("/api/data/data_connectors")
assert response.status_code == 200, response.text
assert len(response.json) == 0
_, response = await sanic_client.get(f"/api/data/projects/{project_id}/data_connector_links")
assert len(response.json) == 0

# Add user to the project
payload = [
{
"id": regular_user.id,
"role": "viewer",
}
]
_, response = await sanic_client.patch(
f"/api/data/projects/{project_id}/members", headers=member_1_headers, json=payload
)
assert response.status_code == 200, response.text

# Now since the user is part of the project they should see only the project DC but not the private one from
# the group that user does not have access to
_, response = await sanic_client.get("/api/data/data_connectors", headers=user_headers)
assert response.status_code == 200, response.text
assert len(response.json) == 1
assert response.json[0]["id"] == project_dc_id
_, response = await sanic_client.get(f"/api/data/projects/{project_id}/data_connector_links", headers=user_headers)
assert response.json[0]["data_connector_id"] == project_dc_id

0 comments on commit 9c50b09

Please sign in to comment.