Skip to content

Commit

Permalink
Support client creds in globus timer create transfer (#957)
Browse files Browse the repository at this point in the history
* Drop GCSv4 autoactivation from timer create

Signed-off-by: Ada <[email protected]>

* Allow timer creation using client credentials

Signed-off-by: Ada <[email protected]>

* Add changelog fragment

Signed-off-by: Ada <[email protected]>

* Add mock consent tree to client creds test

Signed-off-by: Ada <[email protected]>

---------

Signed-off-by: Ada <[email protected]>
  • Loading branch information
ada-globus authored Mar 1, 2024
1 parent c50985a commit 4071606
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Enhancements

* Add support for client credentials in `globus timer create transfer`.
94 changes: 45 additions & 49 deletions src/globus_cli/commands/timer/create/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,9 @@ def transfer_command(
If you use `--batch` and supply a SOURCE_PATH and/or DEST_PATH via the commandline,
these paths will be used as dir prefixes to any paths read from the `--batch` input.
"""
from globus_cli.services.transfer import add_batch_to_transfer_data, autoactivate
from globus_cli.services.transfer import add_batch_to_transfer_data

auth_client = login_manager.get_auth_client()
timer_client = login_manager.get_timer_client()
transfer_client = login_manager.get_transfer_client()

source_endpoint, cmd_source_path = source
Expand Down Expand Up @@ -218,13 +217,6 @@ def transfer_command(
now = datetime.datetime.now().isoformat()
name = f"CLI Created Timer [{now}]"

# Check endpoint activation, figure out scopes needed.

# the autoactivate helper may present output and exit in the case of v4 endpoints
# which need activation (e.g. OA4MP)
autoactivate(transfer_client, source_endpoint, if_expires_in=86400)
autoactivate(transfer_client, dest_endpoint, if_expires_in=86400)

# check if either source or dest requires the data_access scope, and if so
# prompt the user to go through the requisite login flow
source_epish = Endpointish(source_endpoint, transfer_client=transfer_client)
Expand All @@ -238,36 +230,33 @@ def transfer_command(
# this list will only be populated *if* one of the two endpoints requires
# data_access, so if it's empty, we can skip any handling
if needs_data_access:
# if the user is using client credentials, we cannot support the incremental
# auth step in the current implementation
#
# TODO: think through how we can use the client creds to request the
# requisite token in this case; it should be possible
if is_client_login():
raise click.UsageError(
"Unsupported operation. When using client credentials, "
"'globus timer create transfer' does not currently support "
"collections which use the data_access scope: "
f"{','.join(needs_data_access)}"
)

request_data_access = _derive_needed_scopes(auth_client, needs_data_access)

if request_data_access:
scope_request_opts = " ".join(
f"--timer-data-access '{target}'" for target in request_data_access
)
click.echo(
f"""\
A collection you are trying to use in this timer requires you to grant consent
for the Globus CLI to access it.
Please run
globus session consent {scope_request_opts}
to login with the required scopes."""
)
click.get_current_context().exit(4)
scopes_needed = _derive_needed_scopes(needs_data_access)
# If it's not a client login, we need to check
# that the user has the required scopes
if not is_client_login():
request_data_access = _derive_missing_scopes(auth_client, scopes_needed)

if request_data_access:
scope_request_opts = " ".join(
f"--timer-data-access '{target}'" for target in request_data_access
)
click.echo(
f"""\
A collection you are trying to use in this timer requires you to grant consent
for the Globus CLI to access it.
Please run
globus session consent {scope_request_opts}
to login with the required scopes."""
)
click.get_current_context().exit(4)

# Otherwise, add requirements to the LoginManager
login_manager.add_requirement(
globus_sdk.TimerClient.scopes.resource_server,
scopes=list(scopes_needed.values()),
)

transfer_data = globus_sdk.TransferData(
source_endpoint=source_endpoint,
Expand Down Expand Up @@ -296,23 +285,16 @@ def transfer_command(
else: # unreachable
raise NotImplementedError()

timer_client = login_manager.get_timer_client()
body = globus_sdk.TransferTimer(name=name, schedule=schedule, body=transfer_data)
response = timer_client.create_timer(body)
display(response["timer"], text_mode=TextMode.text_record, fields=FORMAT_FIELDS)


def _derive_needed_scopes(
auth_client: CustomAuthClient,
needs_data_access: list[str],
) -> list[str]:
# read the identity ID stored from the login flow
user_identity_id = get_current_identity_id()

# get the user's Globus CLI consents
consents = auth_client.get_consents(user_identity_id)

# check the 'needs_data_access' scope names against the 3rd-order dependencies
# of the Timer scope and record the names of the ones which we need to request
) -> dict[str, MutableScope]:
# Render the fully nested scope strings for each target
scopes_needed = {}
for target in needs_data_access:
target_scope = GCSCollectionScopeBuilder(target).data_access
Expand All @@ -322,7 +304,21 @@ def _derive_needed_scopes(
globus_sdk.TransferClient.scopes.all,
target_scope,
)
return scopes_needed


def _derive_missing_scopes(
auth_client: CustomAuthClient,
scopes_needed: dict[str, MutableScope],
) -> list[str]:
# read the identity ID stored from the login flow
user_identity_id = get_current_identity_id()

# get the user's Globus CLI consents
consents = auth_client.get_consents(user_identity_id)

# check the 'needs_data_access' scope names against the 3rd-order dependencies
# of the Timer scope and record the names of the ones which we need to request
will_request_data_access: list[str] = []
for name, scope_object in scopes_needed.items():
if not consents.contains_scopes([scope_object]):
Expand Down
8 changes: 5 additions & 3 deletions tests/functional/timer/test_transfer_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,9 @@ def test_timer_creation_errors_on_data_access_with_client_creds(
src = make_non_ha_mapped_collection()
dst = ep_for_timer

result = run_line(
setup_timer_consent_tree_response("fake_client_id", src)

run_line(
[
"globus",
"timer",
Expand All @@ -455,7 +457,7 @@ def test_timer_creation_errors_on_data_access_with_client_creds(
"--interval",
"60m",
],
assert_exit_code=2,
)

assert "Unsupported operation." in result.stderr
req = get_last_request()
assert req.url.startswith("https://timer")

0 comments on commit 4071606

Please sign in to comment.