Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

### Enhancements

* Added the parameter `globus session update --scope` to specify additional scopes
to request when updating session tokens.
51 changes: 42 additions & 9 deletions src/globus_cli/commands/flows/start.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from __future__ import annotations

import contextlib
import os
import string
import typing as t
import uuid

import click
import globus_sdk
from globus_sdk.gare import to_gare

from globus_cli._click_compat import shim_get_metavar
from globus_cli.commands.flows._fields import flow_run_format_fields
Expand All @@ -21,6 +23,7 @@
)
from globus_cli.termio import display
from globus_cli.types import JsonValue
from globus_cli.utils import CLIAuthRequirementsError

if t.TYPE_CHECKING:
from click.shell_completion import CompletionItem
Expand Down Expand Up @@ -250,16 +253,46 @@ def start_command(

flow_client = login_manager.get_specific_flow_client(flow_id)
auth_client = login_manager.get_auth_client()

response = flow_client.run_flow(
body=input_document_json,
label=label,
tags=list(tags),
run_managers=list(managers),
run_monitors=list(monitors),
activity_notification_policy=notify_policy,
)
flow_scope = flow_client.scopes.user.scope_string

with scope_injected_into_raised_gares(flow_scope):
response = flow_client.run_flow(
body=input_document_json,
label=label,
tags=list(tags),
run_managers=list(managers),
run_monitors=list(monitors),
activity_notification_policy=notify_policy,
)

fields = flow_run_format_fields(auth_client, response.data)

display(response, fields=fields, text_mode=display.RECORD)


@contextlib.contextmanager
def scope_injected_into_raised_gares(scope: str) -> t.Iterator[None]:
"""
A context manager which catches GARE-convertible GlobusAPIErrors and reraises
as an exact copy of the original GARE but with the supplied scope injected.

Due to SDK Error mutability limitations, these GAREs are raised as
CLIAuthRequirementsErrors. All other categories of errors raise as normal.

:param scope: The scope to inject into any caught GAREs.
:raises CLIAuthRequirementsError: with modified GARE if a GARE-compatible
GlobusAPIError is caught.
"""

try:
yield
except globus_sdk.GlobusAPIError as e:
gare = to_gare(e)
if gare:
scopes = gare.authorization_parameters.required_scopes or []
if scope not in scopes:
scopes.append(scope)

gare.authorization_parameters.required_scopes = scopes
raise CLIAuthRequirementsError("", gare=gare)
raise
9 changes: 9 additions & 0 deletions src/globus_cli/commands/session/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ def _update_session_params_identities_case(
@click.argument(
"identities", type=IdentityType(allow_domains=True), nargs=-1, required=False
)
@click.option(
"--scope",
"scopes",
multiple=True,
type=str,
help="One or more additional scope strings to request during authentication",
)
@click.option(
"--policy",
"policies",
Expand All @@ -101,6 +108,7 @@ def session_update(
login_manager: LoginManager,
*,
identities: tuple[ParsedIdentity, ...],
scopes: tuple[str, ...],
no_local_server: bool,
policies: list[str] | None,
all: bool,
Expand Down Expand Up @@ -156,5 +164,6 @@ def session_update(
"\nYou have successfully updated your CLI session.\n"
"Use 'globus session show' to see the updated session."
),
scopes=list(scopes),
session_params=session_params,
)
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def handle_internal_auth_requirements(
)
def session_hook(exception: globus_sdk.GlobusAPIError) -> None:
"""
Expects an exception with a valid authorization_paramaters info field.
Expects an exception with a valid authorization_parameters info field.
"""
message = exception.info.authorization_parameters.session_message
if message:
Expand Down
27 changes: 16 additions & 11 deletions src/globus_cli/exception_handling/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import os

import click
import globus_sdk
import globus_sdk.gare

from globus_cli.login_manager import is_client_login
from globus_cli.types import JsonValue
Expand Down Expand Up @@ -107,9 +107,11 @@ def emit_session_update_message(
policies: list[str] | None,
identities: list[str] | None,
domains: list[str] | None,
scopes: list[str] | None = None,
message: str = DEFAULT_SESSION_REAUTH_MESSAGE,
) -> None:
click.echo(message)
scope_args = "".join(f" --scope '{s}'" for s in scopes or ())

if identities or domains:
# check both values in this assignment to convince mypy of correctness
Expand All @@ -119,18 +121,18 @@ def emit_session_update_message(
update_target = " ".join(target_list)
click.echo(
"\nPlease run:\n\n"
f" globus session update {update_target}\n\n"
f" globus session update {update_target}{scope_args}\n\n"
"to re-authenticate with the required identities."
)
elif policies:
click.echo(
"\nPlease run:\n\n"
f" globus session update --policy '{','.join(policies)}'\n\n"
f" globus session update --policy '{','.join(policies)}'{scope_args}\n\n"
"to re-authenticate with the required identities."
)
else:
click.echo(
'\nPlease use "globus session update" to re-authenticate '
f'\nPlease use "globus session update {scope_args}" to re-authenticate '
"with specific identities."
)

Expand All @@ -155,22 +157,25 @@ def emit_message_for_gare(
gare: globus_sdk.gare.GARE, message: str | None = None
) -> None:
required_scopes = gare.authorization_parameters.required_scopes
if required_scopes:
emit_consent_required_message(
required_scopes=required_scopes,
message=message or DEFAULT_CONSENT_REAUTH_MESSAGE,
)

session_policies = gare.authorization_parameters.session_required_policies
session_identities = gare.authorization_parameters.session_required_identities
session_domains = gare.authorization_parameters.session_required_single_domain
if session_policies or session_identities or session_domains:

requires_update = bool(session_policies or session_identities or session_domains)

if requires_update:
emit_session_update_message(
policies=session_policies,
identities=session_identities,
domains=session_domains,
scopes=required_scopes,
message=message or DEFAULT_SESSION_REAUTH_MESSAGE,
)
elif required_scopes:
emit_consent_required_message(
required_scopes=required_scopes,
message=message or DEFAULT_CONSENT_REAUTH_MESSAGE,
)


def pretty_json(data: JsonValue, compact: bool = False) -> str:
Expand Down
9 changes: 6 additions & 3 deletions tests/functional/flows/test_start_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import pytest
import responses
from globus_sdk.scopes import SpecificFlowScopes
from globus_sdk.testing import RegisteredResponse, load_response


Expand Down Expand Up @@ -84,7 +85,8 @@ def test_start_flow_prompts_session_reconsent_on_gare(run_line, add_flow_login):
instruct the user to update their session based on the supplied GARE.
"""
flow_id = str(uuid.uuid4())
required_policy_id = str(uuid.uuid4())
flow_scope = SpecificFlowScopes(flow_id).user
policy_id = str(uuid.uuid4())

add_flow_login(flow_id)

Expand All @@ -100,7 +102,7 @@ def test_start_flow_prompts_session_reconsent_on_gare(run_line, add_flow_login):
"Globus Flows detected an unsatisfied session policy for this "
"resource."
),
"session_required_policies": [required_policy_id],
"session_required_policies": [policy_id],
},
"error": {
"code": "AUTHENTICATION_POLICY_REQUIRED",
Expand All @@ -116,7 +118,8 @@ def test_start_flow_prompts_session_reconsent_on_gare(run_line, add_flow_login):

result = run_line(f"globus flows start {flow_id} --input {{}}", assert_exit_code=4)

assert f"globus session update --policy '{required_policy_id}'" in result.stdout
login_cmd = f"globus session update --policy '{policy_id}' --scope '{flow_scope}'"
assert login_cmd in result.stdout


def test_start_flow_rejects_non_object_input(run_line, add_flow_login):
Expand Down
2 changes: 2 additions & 0 deletions tests/functional/flows/test_update_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ def test_update_flow_text_output(run_line, load_identities_for_flow):
"Keywords",
"Owner",
"Subscription ID",
"High Assurance",
"Authentication Policy ID",
"Created At",
"Updated At",
"Administrators",
Expand Down
31 changes: 25 additions & 6 deletions tests/functional/test_session_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,18 @@ def test_username_flow(run_line, userinfo_mocker, mock_remote_session, mock_link
assert call_kwargs["session_params"]["session_required_identities"] == user_id


def test_domain_flow(run_line, userinfo_mocker, mock_remote_session, mock_link_flow):
@pytest.mark.parametrize("with_scopes", [True, False])
def test_domain_flow(
run_line, userinfo_mocker, mock_remote_session, mock_link_flow, with_scopes
):
mock_remote_session.return_value = True
userinfo_mocker.configure_unlinked()

result = run_line("globus session update uchicago.edu")
scopes_args = ""
if with_scopes:
scopes_args = " --scope urn:globus:auth:scope:example.api:all --scope foo"

result = run_line(f"globus session update uchicago.edu{scopes_args}")

assert "You have successfully updated your CLI session." in result.output

Expand All @@ -73,8 +80,9 @@ def test_domain_flow(run_line, userinfo_mocker, mock_remote_session, mock_link_f
)


@pytest.mark.parametrize("with_scopes", [True, False])
def test_all_flow(
run_line, userinfo_mocker, mock_remote_session, mock_local_server_flow
run_line, userinfo_mocker, mock_remote_session, mock_local_server_flow, with_scopes
):
mock_remote_session.return_value = False
meta = userinfo_mocker.configure(
Expand All @@ -84,7 +92,11 @@ def test_all_flow(

ids = [x["sub"] for x in meta["identity_set"]]

result = run_line("globus session update --all")
scopes_args = ""
if with_scopes:
scopes_args = " --scope urn:globus:auth:scope:example.api:all --scope foo"

result = run_line(f"globus session update --all{scopes_args}")

assert "You have successfully updated your CLI session." in result.output

Expand All @@ -97,11 +109,18 @@ def test_all_flow(
) == set(ids)


def test_policy_flow(run_line, userinfo_mocker, mock_remote_session, mock_link_flow):
@pytest.mark.parametrize("with_scopes", [True, False])
def test_policy_flow(
run_line, userinfo_mocker, mock_remote_session, mock_link_flow, with_scopes
):
mock_remote_session.return_value = True
userinfo_mocker.configure_unlinked()

result = run_line("globus session update --policy foo,bar")
scopes_args = ""
if with_scopes:
scopes_args = " --scope urn:globus:auth:scope:example.api:all --scope foo"

result = run_line(f"globus session update --policy foo,bar{scopes_args}")

assert "You have successfully updated your CLI session." in result.output

Expand Down