-
Notifications
You must be signed in to change notification settings - Fork 28
[SC 38762] Adding collection role create #1182
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
base: main
Are you sure you want to change the base?
Changes from all commits
39495c3
9a5d427
cb68ac3
ee4569e
4c09912
524f90b
cb39e0d
36d7287
1be8820
dbfb4cf
1a97849
a96a780
8e97d2d
b8e9b1c
3524f18
2ff8829
6f86379
8daa6c5
a479543
8bd9bc7
37e9570
02d161e
1dd8494
ad29988
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| ### Enhancements | ||
|
|
||
| * Add a new command for interacting with collection roles, | ||
| `globus gcs collection role create` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import typing as t | ||
|
|
||
| import globus_sdk | ||
|
|
||
| from globus_cli.termio import Field | ||
| from globus_cli.termio.formatters.auth import PrincipalURNFormatter | ||
|
|
||
|
|
||
| class CollectionRoleFormatter(PrincipalURNFormatter): | ||
| """ | ||
| A formatter for collection roles | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, auth_client: globus_sdk.AuthClient, collection_role: dict[str, t.Any] | ||
| ) -> None: | ||
| super().__init__(auth_client) | ||
| self.add_items(collection_role.get("id")) | ||
| self.add_items(collection_role.get("role", ())) | ||
| self.add_items(collection_role.get("principal")) | ||
|
|
||
|
|
||
| def collection_role_format_fields( | ||
| auth_client: globus_sdk.AuthClient, | ||
| collection_role: dict[str, t.Any], | ||
| ) -> list[Field]: | ||
| """ | ||
| The standard list of fields to render for a collection role. | ||
| :param auth_client: An AuthClient, used to resolve principal URNs. | ||
| :param collection_role: The collection role assignment to format | ||
| """ | ||
| principal = CollectionRoleFormatter(auth_client, collection_role) | ||
|
|
||
| return [ | ||
| Field("ID", "id"), | ||
| Field("Role", "role"), | ||
| Field("Principal", "principal", formatter=principal), | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import typing as t | ||
| import uuid | ||
|
|
||
| import click | ||
| import globus_sdk | ||
|
|
||
| from globus_cli.commands.collection.role._fields import collection_role_format_fields | ||
| from globus_cli.login_manager import LoginManager | ||
| from globus_cli.parsing import collection_id_arg, command | ||
| from globus_cli.termio import display | ||
| from globus_cli.utils import resolve_principal_urn | ||
|
|
||
| _VALID_ROLES = t.Literal[ | ||
| "access_manager", "activity_manager", "activity_monitor", "administrator" | ||
| ] | ||
|
|
||
|
|
||
| @command("create") | ||
| @collection_id_arg | ||
| @click.argument("ROLE", type=click.Choice(t.get_args(_VALID_ROLES)), metavar="ROLE") | ||
| @click.argument("PRINCIPAL", type=str, required=False) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have two notes on this being an optional argument. The first is a general "CLI feel" item: if it's optional, I think it should be an option, e.g. The second is... Should this be optional? I'm not convinced that's the correct way to represent the GCS API. I think defaulting to the current user is a very surprising behavior: since the API itself is for manipulating roles, the current user must have some role already. In all likelihood, a user running role create is creating roles for other users and groups. In summary:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed on the first point, option = optional; argument = required. The second point is a change that I suggested to James. I'll ask in the gcs devchat; seems like a pretty domain specific usage question.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Maybe this is where our perspectives diverge. I don't think it's purely domain specific. I think we should consider this in the context of the following commands:
Of these,
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. gcs weighed in, let's go back to your original behavior James (no defaulting to the current user). Sorry for the flip-flop. From Jason:
|
||
| @click.option( | ||
| "--principal-type", | ||
| type=click.Choice(["identity", "group"]), | ||
| help="Qualifier to specify what type of principal (identity or group) is provided.", | ||
| ) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note For any other reviewers, this That said, I'm very much interested in developing a better interface for passing these inputs. I think we could design something as a replacement which takes URNs, short-forms like |
||
| @LoginManager.requires_login("transfer") | ||
| def create_command( | ||
| login_manager: LoginManager, | ||
| *, | ||
| collection_id: uuid.UUID, | ||
| role: t.Literal[ | ||
| "access_manager", "activity_manager", "activity_monitor", "administrator" | ||
| ], | ||
| principal: str | None, | ||
| principal_type: t.Literal["identity", "group"] | None, | ||
| ) -> None: | ||
| """ | ||
| Create a role assignment on a Collection. | ||
| ROLE must be one of: | ||
| "administrator", | ||
| "access_manager", | ||
| "activity_manager", | ||
| "activity_monitor" | ||
| If a PRINCIPAL value is not provided the primary identity of the logged in user will | ||
| be used. | ||
| If a PRINCIPAL value is provided, it must be a username, UUID, or URN associated | ||
| with a globus identity or group. | ||
| If UUID, use `--principal-type` to specify the type (defaults to "identity"). | ||
| """ | ||
|
|
||
| gcs_client = login_manager.get_gcs_client(collection_id=collection_id) | ||
| auth_client = login_manager.get_auth_client() | ||
|
|
||
| # If Principal argument isn't provided, determine user's primary identity | ||
| if principal is None: | ||
| userinfo = auth_client.userinfo() | ||
| principal = userinfo["sub"] | ||
|
|
||
| # Format the principal into a URN | ||
| principal_urn = resolve_principal_urn( | ||
| auth_client=auth_client, | ||
| principal_type=principal_type, | ||
| principal=principal, | ||
| ) | ||
|
|
||
| res = gcs_client.create_role( | ||
| globus_sdk.GCSRoleDocument( | ||
| DATA_TYPE="role#1.0.0", | ||
| collection=collection_id, | ||
| role=role, | ||
| principal=principal_urn, | ||
| ) | ||
| ) | ||
|
|
||
| fields = collection_role_format_fields(auth_client, res.data) | ||
|
|
||
| display(res, text_mode=display.RECORD, fields=fields) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| from globus_sdk.testing import load_response_set | ||
|
|
||
|
|
||
| def test_successful_gcs_collection_role_creation( | ||
| run_line, | ||
| add_gcs_login, | ||
| get_identities_mocker, | ||
| ): | ||
| # setup data for the collection_id -> endpoint_id lookup | ||
| # and create dummy credentials for the test to run against that GCS | ||
| meta = load_response_set("cli.collection_operations").metadata | ||
|
|
||
| collection_id = meta["mapped_collection_id"] | ||
| endpoint_id = meta["endpoint_id"] | ||
| role = meta["role"] | ||
| role_id = meta["role_id"] | ||
| user_id = meta["identity_id"] | ||
| add_gcs_login(endpoint_id) | ||
|
|
||
| role = "activity_monitor" | ||
|
|
||
| # Mock the Get Identities API (Auth) | ||
| # so that CLI output rendering can show a username | ||
| user_meta = get_identities_mocker.configure_one(id=user_id).metadata | ||
| username = user_meta["username"] | ||
|
|
||
| # now test the command and confirm that a successful role creation is reported | ||
| run_line( | ||
| ["globus", "gcs", "collection", "role", "create", collection_id, role, user_id], | ||
| search_stdout=[ | ||
| ("ID", role_id), | ||
| ("Role", role), | ||
| ("Principal", username), | ||
| ], | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,4 @@ | ||
| import uuid | ||
|
|
||
| from globus_sdk.testing import RegisteredResponse, load_response_set | ||
| from globus_sdk.testing import load_response_set | ||
|
|
||
|
|
||
| def test_successful_gcs_collection_role_list( | ||
|
|
@@ -13,32 +11,11 @@ def test_successful_gcs_collection_role_list( | |
| meta = load_response_set("cli.collection_operations").metadata | ||
| endpoint_id = meta["endpoint_id"] | ||
| collection_id = meta["mapped_collection_id"] | ||
| role = meta["role"] | ||
| role_id = meta["role_id"] | ||
| user_id = meta["identity_id"] | ||
| add_gcs_login(endpoint_id) | ||
|
|
||
| user_id = str(uuid.UUID(int=2)) | ||
|
|
||
| # mock the responses for the Get Role API (GCS) | ||
| RegisteredResponse( | ||
| service="gcs", | ||
| path="/roles", | ||
| json={ | ||
| "DATA_TYPE": "result#1.1.0", | ||
| "code": "success", | ||
| "data": [ | ||
| { | ||
| "DATA_TYPE": "role#1.0.0", | ||
| "collection": f"{collection_id}", | ||
| "id": f"{user_id}", | ||
| "principal": f"urn:globus:auth:identity:{user_id}", | ||
| "role": "administrator", | ||
| } | ||
| ], | ||
| "detail": "success", | ||
| "has_next_page": False, | ||
| "http_response_code": 200, | ||
| }, | ||
| ).add() | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see that this moved from being a code construction here to being defined in the YAML fixture files. I've been trying to migrate CLI and SDK off of the YAML fixtures, and onto the pattern used here of imperatively defined test data. It's a slow transition because we can't pull support all at once (or it would break a lot of tests). Ideally we would not make any additions to the fixture files, but I won't insist on that for all new work at this stage. For now, I just don't want us to migrate things which are moving away from that paradigm "backwards", in the wrong direction. Note My rationale for getting away from the YAML is that it has in practice not worked out very well for extensibility and has created some brittle dependencies between test files. Most notably, when multiple distinct files are coordinated, this is done by carefully keeping IDs in sync between distinct files. The newer way to manage data dependencies I've worked up is to construct configurators like the get_identities_mocker (seen below this comment). It still doesn't cover all needs, but I'm interested in growing that suite of tools.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Absolutely and will do! |
||
| # Mock the Get Identities API (Auth) | ||
| # so that CLI output rendering can show a username | ||
| user_meta = get_identities_mocker.configure_one(id=user_id).metadata | ||
|
|
@@ -49,8 +26,8 @@ def test_successful_gcs_collection_role_list( | |
| run_line( | ||
| ["globus", "gcs", "collection", "role", "list", collection_id], | ||
| search_stdout=[ | ||
| ("ID", user_id), | ||
| ("Role", "administrator"), | ||
| ("ID", role_id), | ||
| ("Role", role), | ||
| ("Principal", username), | ||
| ], | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear to me why this additional formatter exists. Could you help me understand why it's here?
By my understanding, all three of these calls pass strings, which are then iterated on with an attempt to parse each character in those strings:
globus-cli/src/globus_cli/termio/formatters/auth.py
Lines 61 to 68 in e7d2804
Since no single character will successfully parse as a principal URN, I'd expect the three calls to do nothing.
I see it used below in a field where it is formatting the
principalfield in a response. Would aPrincipalURNFormatter, which is what was in use inrole listprior to this PR, not suffice?