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

Change API to be able to assign roles to namespaces #607

Merged
merged 27 commits into from
Dec 5, 2023

Conversation

nkaretnikov
Copy link
Contributor

@nkaretnikov nkaretnikov commented Oct 3, 2023

Fixes #491.

@netlify
Copy link

netlify bot commented Oct 3, 2023

Deploy Preview for kaleidoscopic-dango-0cf31d canceled.

Name Link
🔨 Latest commit 70c20f3
🔍 Latest deploy log https://app.netlify.com/sites/kaleidoscopic-dango-0cf31d/deploys/65685d3a6572db00080f4891

@nkaretnikov

This comment was marked as outdated.

tests/test_api.py Outdated Show resolved Hide resolved
@nkaretnikov nkaretnikov force-pushed the role-mappings-api-491 branch 3 times, most recently from 67c271e to 0b19764 Compare October 16, 2023 18:06
Otherwise, nothing is printed when `role_mappings_version` is set
incorrectly.

This

```
c.RBACAuthorizationBackend.role_mappings_version = 42
```

now prints

```
c.RBACAuthorizationBackend.role_mappings_version: invalid role mappings
version: 42, expected: (1, 2)
```
@nkaretnikov
Copy link
Contributor Author

nkaretnikov commented Dec 1, 2023

Migrating data from v1 to v2 role mappings will be done in a separate PR because it can cause issues during migration, which might require manual intervention. So a standalone function will be provided for those who need it. #681

@app.exception_handler(Exception)
async def exception_handler(request, exc):
print(exc)
return await http_exception_handler(request, exc)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this to print an exception to the terminal when an invalid role_mappings_version value is used. Previously, nothing was printed except "Invalid server error".

for row in raw_role_mappings:
db_role_mappings[row["entity"]].add(row["role"])

return db_role_mappings
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code used to be in database_role_bindings. I moved it to the start of the class to use it in _role_mappings_versions.

@@ -140,36 +141,191 @@ def test_authorization(conda_store, entity_bindings, arn, permissions, authorize
assert authorized == authorization.authorize(entity, arn, permissions)


def test_end_to_end_auth_flow(conda_store):
@pytest.mark.parametrize("role_mappings_version", [1, 2])
Copy link
Contributor Author

@nkaretnikov nkaretnikov Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this test to match the newly added test_end_to_end_auth_flow_v2. The v1 test calls HTTP APIs that use the v1 role mappings table. The v2 test does the same for the v2 table. Both tests check that auth works after inserting roles into the corresponding DB table.

response.raise_for_status()

# Should fail again
assert authorize() is False
Copy link
Contributor Author

@nkaretnikov nkaretnikov Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that in the v2 table the relationship between namespaces is reversed compared to v1. That's why this performs API requests to other_namespace and sets other_namespace to namespace in post. This is intended.

The v1 table says: "I give myself access to this namespace"
The v2 table says: "I allow this namespace access"

Permissions.SETTING_READ,
Permissions.SETTING_UPDATE,
},
)
Copy link
Contributor Author

@nkaretnikov nkaretnikov Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the above block should be indented.

@nkaretnikov nkaretnikov changed the title WIP: Change API to be able to assign roles to namespaces Change API to be able to assign roles to namespaces Dec 3, 2023
@nkaretnikov nkaretnikov marked this pull request as ready for review December 3, 2023 21:46
@nkaretnikov
Copy link
Contributor Author

I've reviewed the code, not planning to make any changes. Ready for review.

@nkaretnikov
Copy link
Contributor Author

For reviewers: this is a huge PR, I suggest you start with conda-store-server/tests/test_auth.py and docs/administration.md. The doc explains how this is supposed to be used. The auth test provides end-to-end examples for v1 and v2 and shows how this affects authorization.

Copy link
Contributor

@dcmcand dcmcand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments in this review are mostly stylistic and are not blocking for approval.

if namespace is None:
raise ValueError(f"Namespace='{name}' not found")

nrm = aliased(orm.NamespaceRoleMappingV2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please have more descriptive variable names here? Readability is part of maintainability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I agree. But not here. I deliberately use short names so that queries don't get split up over multiple lines by the linter, which would be hard to read. All these DB functions are very small, so there's no concern of this clashing with something else causing confusion. I couldn't think of a short name for NamespaceRoleMapping other than using the first letters.

conda-store-server/conda_store_server/api.py Show resolved Hide resolved
conda-store-server/conda_store_server/api.py Show resolved Hide resolved
namespace = relationship(Namespace, foreign_keys=[namespace_id])

# ... for other namespace
other_namespace_id = Column(Integer, ForeignKey("namespace.id"), nullable=False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, these names don't make it clear which namespace has permissions to which.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in another comment above.

@dcmcand
Copy link
Contributor

dcmcand commented Dec 5, 2023

One thought that occurred to me is that introducing the version to our routes actually is breaking if anyone has previously had naked routes. I think adding a router for naked routes that redirects to v1 of the route and documenting this would handle that issue. This could be a seperate issue/pr if that is simpler, but without it this will be a breaking change.

@nkaretnikov
Copy link
Contributor Author

One thought that occurred to me is that introducing the version to our routes actually is breaking if anyone has previously had naked routes.

Did you mean to reply to the router_ui thread? This PR doesn't make changes to existing routes.

@dcmcand
Copy link
Contributor

dcmcand commented Dec 5, 2023

One thought that occurred to me is that introducing the version to our routes actually is breaking if anyone has previously had naked routes.

Did you mean to reply to the router_ui thread? This PR doesn't make changes to existing routes.

No, i meant in regards to the api. I thought that we had discussed making the default v1, but if this doesn't touch existing routes then you can disregard this comment.

@nkaretnikov
Copy link
Contributor Author

Yeah, no action is needed here. I'll mention this for whoever is reading these later. The API I'm using is already versioned:

router_api = APIRouter(
    tags=["api"],
    prefix="/api/v1",
)

@@ -71,6 +71,108 @@ def test_namespace_role_mapping(db):
NamespaceRoleMapping(namespace=namespace, namespace_id=namespace.id, entity="*/*")


def test_namespace_role_mapping_v2(db):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the detailed tests on this feature

},
primary_namespace=namespace,
# No default roles
role_bindings={},
)
)

token_model = authentication.authenticate(token)

authorization = RBACAuthorizationBackend(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pulling all this auth stuff that is setup for the auth tests into a helper function would help readability of these tests imo. This won't block approval, but would help maintainability I think.

other_namespace_name3 = "pytest-other-namespace3"

# Starts with no namespaces
assert len(api.list_namespaces(db).all()) == 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be simpler to just clear out the namespaces like you do in the auth tests than to assert here.

assert len(roles) == 1
roles = api.get_other_namespace_roles(db, namespace_name)
db.commit()
assert len(roles) == 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, relying on having a pure db and checking lengths has led to flaky tests for me in the past, especially if you ever try to run tests in parallel. I always prefer to check for concrete records than for lengths.

@nkaretnikov nkaretnikov merged commit 8c92b55 into conda-incubator:main Dec 5, 2023
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done 💪🏾
Development

Successfully merging this pull request may close these issues.

Improvements to the namespace Role Mappings
4 participants