Skip to content

Commit 4b5027b

Browse files
authored
Merge pull request #1 from globusonline/sc-45485-add-basic-auth-mechs
Add basic authentication mechanisms
2 parents dcc6dfe + 11a099b commit 4b5027b

File tree

9 files changed

+233
-23
lines changed

9 files changed

+233
-23
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
/.coverage*
44
poetry.lock
55
__pycache__/
6+
.tox/
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Added
2+
-----
3+
4+
* Add support for ``ClientApp`` and ``UserApp`` authentication. Use
5+
``ClientApp`` when ``GLOBUS_REGISTERED_API_CLIENT_ID``
6+
and ``GLOBUS_REGISTERED_API_CLIENT_SECRET`` environment variables
7+
are set, otherwise use ``UserApp`` with a registered native client.
8+
* Add ``whoami`` command to display authenticated user information with
9+
``--format`` option for text or JSON output.
10+
* Add ``logout`` command to revoke tokens for ``UserApp`` and ``ClientApp``
11+
sessions.
12+
13+
Removed
14+
-------
15+
16+
* Remove ``bogus`` command.

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ classifiers = [
2020

2121
dependencies = [
2222
"click >=8,<9",
23+
"globus-sdk >=4",
2324
]
2425

2526
[project.urls]
2627
Source = "https://github.com/globusonline/globus-registered-api"
2728

2829
[project.scripts]
29-
globus-registered-api = "globus_registered_api.cli:group"
30-
gra = "globus_registered_api.cli:group"
30+
globus-registered-api = "globus_registered_api.cli:cli"
31+
gra = "globus_registered_api.cli:cli"
3132

3233
[build-system]
3334
requires = ["poetry-core>=2.0.0,<3.0.0"]
@@ -69,6 +70,7 @@ source = [
6970
[tool.coverage.report]
7071
skip_covered = true
7172
fail_under = 50
73+
show_missing = true
7274

7375

7476
# flake8

src/globus_registered_api/cli.py

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,83 @@
22
# https://github.com/globusonline/globus-registered-api
33
# Copyright 2025 Globus <support@globus.org>
44
# SPDX-License-Identifier: MIT
5+
import json
6+
import os
7+
8+
from globus_sdk import AuthClient, ClientApp, UserApp
59

610
import click
711

8-
group = click.Group()
12+
RAPI_NATIVE_CLIENT_ID = "9dc7dfff-cfe8-4339-927b-28d29e1b2f42"
13+
14+
15+
def _create_globus_app() -> UserApp | ClientApp:
16+
"""
17+
Create and return a Globus app based on environment variables.
18+
19+
Checks for GLOBUS_CLIENT_ID and GLOBUS_CLIENT_SECRET environment variables.
20+
If both are present, creates a ClientApp for client credentials authentication.
21+
Otherwise, creates a UserApp with a registered native client.
22+
23+
:return: A ClientApp if both environment variables are set, otherwise a UserApp
24+
:raises ValueError: If only one of the required environment variables is set
25+
"""
26+
client_id = os.getenv("GLOBUS_REGISTERED_API_CLIENT_ID")
27+
client_secret = os.getenv("GLOBUS_REGISTERED_API_CLIENT_SECRET")
28+
app_name = "globus-registered-api-cli"
29+
30+
# Validate: both or neither
31+
if bool(client_id) ^ bool(client_secret):
32+
raise ValueError(
33+
"Both GLOBUS_CLIENT_ID and GLOBUS_CLIENT_SECRET must be set, or neither."
34+
)
35+
36+
if client_id and client_secret:
37+
return ClientApp(app_name=app_name, client_id=client_id, client_secret=client_secret)
38+
else:
39+
return UserApp(app_name=app_name, client_id=RAPI_NATIVE_CLIENT_ID)
40+
41+
42+
def _create_auth_client(app: UserApp | ClientApp) -> AuthClient:
43+
"""
44+
Create an AuthClient for the given app.
45+
46+
:param app: A Globus app instance to use for authentication
47+
:return: An AuthClient configured with the provided app
48+
"""
49+
return AuthClient(app=app)
50+
51+
52+
@click.group()
53+
@click.pass_context
54+
def cli(ctx: click.Context) -> None:
55+
"""Globus Registered API Command Line Interface."""
56+
ctx.obj = _create_globus_app()
57+
58+
59+
@cli.command()
60+
@click.option("--format", type=click.Choice(["json", "text"]), default="text")
61+
@click.pass_context
62+
def whoami(ctx: click.Context, format: str) -> None:
63+
"""
64+
Display information about the authenticated user.
65+
"""
66+
app: UserApp | ClientApp = ctx.obj
67+
auth_client = _create_auth_client(app)
68+
res = auth_client.userinfo()
969

70+
if format == "text":
71+
click.echo(res["preferred_username"])
72+
else:
73+
click.echo(json.dumps(res.data, indent=2))
1074

11-
@group.command()
12-
def bogus() -> None:
13-
"""Print 'bogus'."""
1475

15-
print("bogus")
76+
@cli.command()
77+
@click.pass_context
78+
def logout(ctx: click.Context) -> None:
79+
"""
80+
Log out the current user by revoking all tokens.
81+
"""
82+
app: UserApp | ClientApp = ctx.obj
83+
app.logout()
84+
click.echo("Logged out successfully.")

tests/conftest.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import typing as t
2+
3+
import pytest
4+
5+
from click.testing import CliRunner
6+
7+
8+
@pytest.fixture
9+
def mock_client_env(monkeypatch):
10+
monkeypatch.setenv("GLOBUS_REGISTERED_API_CLIENT_ID", "test-id")
11+
monkeypatch.setenv("GLOBUS_REGISTERED_API_CLIENT_SECRET", "test-secret")
12+
13+
14+
@pytest.fixture
15+
def cli_runner() -> t.Generator[CliRunner, None, None]:
16+
return CliRunner()
17+

tests/test_bogus.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

tests/test_cli.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import pytest
2+
from globus_sdk import ClientApp, UserApp
3+
4+
from globus_registered_api.cli import _create_globus_app
5+
6+
7+
@pytest.mark.parametrize("env_var, value", [
8+
("GLOBUS_REGISTERED_API_CLIENT_ID", "test-id"),
9+
("GLOBUS_REGISTERED_API_CLIENT_SECRET", "test-secret")
10+
])
11+
def test_create_globus_app_without_required_env_vars_failure(monkeypatch, env_var, value):
12+
# Arrange
13+
monkeypatch.setenv(env_var, value)
14+
15+
# Act
16+
with pytest.raises(ValueError) as excinfo:
17+
_ = _create_globus_app()
18+
19+
# Assert
20+
assert "Both GLOBUS_CLIENT_ID and GLOBUS_CLIENT_SECRET must be set, or neither." in str(excinfo.value)
21+
22+
23+
def test_create_globus_app_returns_client_app_when_env_vars_set(monkeypatch, mock_client_env):
24+
# Act
25+
app = _create_globus_app()
26+
27+
# Assert
28+
assert isinstance(app, ClientApp)
29+
30+
31+
def test_create_globus_app_returns_user_app_when_env_vars_not_set(monkeypatch):
32+
# Arrange
33+
monkeypatch.delenv("GLOBUS_CLIENT_ID", raising=False)
34+
monkeypatch.delenv("GLOBUS_CLIENT_SECRET", raising=False)
35+
36+
# Act
37+
app = _create_globus_app()
38+
39+
# Assert
40+
assert isinstance(app, UserApp)

tests/test_logout.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
import globus_registered_api.cli
4+
5+
6+
@patch("globus_registered_api.cli._create_globus_app")
7+
def test_logout(mock_create_app, cli_runner):
8+
# Arrange
9+
mock_app = MagicMock()
10+
mock_create_app.return_value = mock_app
11+
12+
# Act
13+
result = cli_runner.invoke(globus_registered_api.cli.cli, ["logout"])
14+
15+
# Assert
16+
assert result.exit_code == 0
17+
mock_app.logout.assert_called_once()
18+
assert "Logged out successfully." in result.output

tests/test_whoami.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# This file is a part of globus-registered-api.
2+
# https://github.com/globusonline/globus-registered-api
3+
# Copyright 2025 Globus <support@globus.org>
4+
# SPDX-License-Identifier: MIT
5+
from unittest.mock import MagicMock, patch
6+
import uuid
7+
8+
import pytest
9+
10+
import globus_registered_api.cli
11+
12+
13+
class MockResponse:
14+
def __init__(self, data):
15+
self.data = data
16+
17+
def __getitem__(self, key):
18+
return self.data[key]
19+
20+
21+
@patch("globus_registered_api.cli._create_auth_client", autospec=True)
22+
def test_whoami_with_user_app(mock_auth_client, cli_runner):
23+
# Arrange
24+
mock_auth = MagicMock()
25+
mock_auth.userinfo.return_value = MockResponse({"preferred_username": "testuser", "email": "testuser@example.com"})
26+
mock_auth_client.return_value = mock_auth
27+
28+
# Act
29+
result = cli_runner.invoke(globus_registered_api.cli.cli, ["whoami"])
30+
31+
# Assert
32+
assert result.exit_code == 0
33+
assert "testuser" in result.output
34+
35+
# Act (json format)
36+
result_json = cli_runner.invoke(globus_registered_api.cli.cli, ["whoami", "--format", "json"])
37+
38+
# Assert
39+
assert result_json.exit_code == 0
40+
assert '"preferred_username": "testuser"' in result_json.output
41+
42+
43+
@patch("globus_registered_api.cli._create_auth_client", autospec=True)
44+
def test_whoami_with_client_app(mock_client_env, cli_runner):
45+
# Arrange
46+
mock_auth = MagicMock()
47+
client_id = str(uuid.uuid4())
48+
mock_auth.userinfo.return_value = MockResponse({"preferred_username": f"{client_id}@clients.auth.globus.org", "email": None})
49+
mock_client_env.return_value = mock_auth
50+
51+
# Act
52+
result = cli_runner.invoke(globus_registered_api.cli.cli, ["whoami"])
53+
54+
# Assert
55+
assert result.exit_code == 0
56+
assert client_id in result.output
57+
58+
# Act (json format)
59+
result_json = cli_runner.invoke(globus_registered_api.cli.cli, ["whoami", "--format", "json"])
60+
61+
# Assert
62+
assert result_json.exit_code == 0
63+
assert f'"preferred_username": "{client_id}@clients.auth.globus.org"' in result_json.output

0 commit comments

Comments
 (0)