Skip to content

Commit c50a714

Browse files
feat: add HTML templates for authentication success and error, and improve session handling in SSO
1 parent 6abe454 commit c50a714

11 files changed

Lines changed: 262 additions & 42 deletions

File tree

.envrc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ if [ -d ".venv" ]; then
1818
fi
1919
fi
2020

21-
pyenv install --skip-existing "$PYTHON_VERSION"
22-
layout pyenv "$PYTHON_VERSION"
23-
python -m pip install -U pip
24-
python -m pip install -q -r requirements.txt
21+
uv venv .venv --python $PYTHON_VERSION
22+
uv pip install -U pip uv
23+
uv pip install -e .
24+
25+
source ./.venv/bin/activate
2526

2627
# Load environment variables from the .env file (for pytest, mainly)
2728
if [ ! -f .env ]; then

cloudsmith_cli/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""Cloudsmith CLI."""
2+
import warnings
3+
24
import click
35
import urllib3
46

57
click.disable_unicode_literals_warning = True
68
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
9+
warnings.filterwarnings("ignore", category=ResourceWarning)

cloudsmith_cli/cli/commands/auth.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from .. import decorators, validators
77
from ..exceptions import handle_api_exceptions
8-
from ..saml import get_idp_url
8+
from ..saml import create_configured_session, get_idp_url
99
from ..webserver import AuthenticationWebRequestHandler, AuthenticationWebServer
1010
from .main import main
1111

@@ -35,9 +35,11 @@ def authenticate(ctx, opts, owner):
3535
)
3636
)
3737

38+
session = create_configured_session(opts)
39+
3840
context_message = "Failed to authenticate via SSO!"
3941
with handle_api_exceptions(ctx, opts=opts, context_msg=context_message):
40-
idp_url = get_idp_url(api_host, owner)
42+
idp_url = get_idp_url(api_host, owner, session=session)
4143
click.echo(
4244
"Opening your organization's SAML IDP URL in your browser: %(idp_url)s"
4345
% {"idp_url": click.style(idp_url, bold=True)}
@@ -51,8 +53,7 @@ def authenticate(ctx, opts, owner):
5153
AuthenticationWebRequestHandler,
5254
api_host=api_host,
5355
owner=owner,
56+
session=session,
57+
debug=opts.debug,
5458
)
5559
auth_server.handle_request()
56-
57-
click.echo()
58-
click.secho("Authentication complete", fg="green")

cloudsmith_cli/cli/commands/whoami.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from ...core.api.user import get_user_brief
66
from .. import decorators
77
from ..exceptions import handle_api_exceptions
8-
from ..utils import maybe_spinner
8+
from ..utils import maybe_print_as_json, maybe_spinner
99
from .main import main
1010

1111

@@ -17,13 +17,30 @@
1717
@click.pass_context
1818
def whoami(ctx, opts):
1919
"""Retrieve your current authentication status."""
20-
click.echo("Retrieving your authentication status from the API ... ", nl=False)
20+
use_stderr = opts.output in ("json", "pretty_json")
21+
22+
click.echo(
23+
"Retrieving your authentication status from the API ... ",
24+
nl=False,
25+
err=use_stderr,
26+
)
2127

2228
context_msg = "Failed to retrieve your authentication status!"
2329
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
2430
with maybe_spinner(opts):
2531
is_auth, username, email, name = get_user_brief()
26-
click.secho("OK", fg="green")
32+
click.secho("OK", fg="green", err=use_stderr)
33+
34+
data = {
35+
"is_authenticated": is_auth,
36+
"username": username,
37+
"email": email,
38+
"name": name,
39+
}
40+
41+
if maybe_print_as_json(opts, data):
42+
return
43+
2744
click.echo("You are authenticated as:")
2845
if not is_auth:
2946
click.secho("Nobody (i.e. anonymous user)", fg="yellow")

cloudsmith_cli/cli/saml.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,35 @@
55
from ..core.api.exceptions import ApiException
66

77

8-
def get_idp_url(api_host, owner):
8+
def create_configured_session(opts):
9+
"""
10+
Create a requests session configured with the options from opts.
11+
"""
12+
session = requests.Session()
13+
14+
if hasattr(opts, "api_ssl_verify") and opts.api_ssl_verify is not None:
15+
session.verify = opts.api_ssl_verify
16+
17+
if hasattr(opts, "api_proxy") and opts.api_proxy:
18+
session.proxies = {"http": opts.api_proxy, "https": opts.api_proxy}
19+
20+
if hasattr(opts, "api_user_agent") and opts.api_user_agent:
21+
session.headers.update({"User-Agent": opts.api_user_agent})
22+
23+
if hasattr(opts, "api_headers") and opts.api_headers:
24+
session.headers.update(opts.api_headers)
25+
26+
return session
27+
28+
29+
def get_idp_url(api_host, owner, session):
930
org_saml_url = "{api_host}/orgs/{owner}/saml/?{params}".format(
1031
api_host=api_host,
1132
owner=owner,
1233
params=urlencode({"redirect_url": "http://localhost:12400"}),
1334
)
1435

15-
org_saml_response = requests.get(org_saml_url, timeout=30)
36+
org_saml_response = session.get(org_saml_url, timeout=30)
1637

1738
try:
1839
org_saml_response.raise_for_status()
@@ -26,18 +47,20 @@ def get_idp_url(api_host, owner):
2647
return org_saml_response.json().get("redirect_url")
2748

2849

29-
def exchange_2fa_token(api_host, two_factor_token, totp_token):
50+
def exchange_2fa_token(api_host, two_factor_token, totp_token, session):
3051
exchange_data = {"two_factor_token": two_factor_token, "totp_token": totp_token}
3152
exchange_url = "{api_host}/user/two-factor/".format(api_host=api_host)
3253

33-
exchange_response = requests.post(
54+
headers = {
55+
"Authorization": "Bearer {two_factor_token}".format(
56+
two_factor_token=two_factor_token
57+
)
58+
}
59+
60+
exchange_response = session.post(
3461
exchange_url,
3562
data=exchange_data,
36-
headers={
37-
"Authorization": "Bearer {two_factor_token}".format(
38-
two_factor_token=two_factor_token
39-
)
40-
},
63+
headers=headers,
4164
timeout=30,
4265
)
4366

@@ -57,16 +80,18 @@ def exchange_2fa_token(api_host, two_factor_token, totp_token):
5780
return (access_token, refresh_token)
5881

5982

60-
def refresh_access_token(api_host, access_token, refresh_token):
83+
def refresh_access_token(api_host, access_token, refresh_token, session):
6184
data = {"refresh_token": refresh_token}
6285
url = "{api_host}/user/refresh-token/".format(api_host=api_host)
6386

64-
response = requests.post(
87+
headers = {
88+
"Authorization": "Bearer {access_token}".format(access_token=access_token)
89+
}
90+
91+
response = session.post(
6592
url,
6693
data=data,
67-
headers={
68-
"Authorization": "Bearer {access_token}".format(access_token=access_token)
69-
},
94+
headers=headers,
7095
timeout=30,
7196
)
7297

cloudsmith_cli/cli/tests/test_saml.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ def test_get_idp_url(self, mock_get_request, mock_response):
3535
mock_get_request.return_value = mock_response
3636
mock_response.json.return_value = {"redirect_url": "response_redirect_url"}
3737

38-
assert get_idp_url(self.api_host, "test_org") == "response_redirect_url"
38+
assert (
39+
get_idp_url(self.api_host, "test_org", session=requests.sessions.Session())
40+
== "response_redirect_url"
41+
)
3942
mock_get_request.assert_called_once_with(
4043
f"{self.api_host}/orgs/test_org/saml/?{self.query_params}", timeout=30
4144
)
@@ -50,7 +53,7 @@ def test_get_idp_url_with_request_error(self, mock_get_request, mock_response):
5053
)
5154

5255
with pytest.raises(ApiException) as exc:
53-
get_idp_url(self.api_host, "test_org")
56+
get_idp_url(self.api_host, "test_org", session=requests.sessions.Session())
5457

5558
assert exc == ApiException(
5659
status=500, headers={"foo": "bar"}, body="Error body"
@@ -66,7 +69,12 @@ def test_exchange_2fa_token(self, mock_post_request, mock_response):
6669
"refresh_token": "refresh_token",
6770
}
6871

69-
assert exchange_2fa_token(self.api_host, "two_factor_token", "totp_token") == (
72+
assert exchange_2fa_token(
73+
self.api_host,
74+
"two_factor_token",
75+
"totp_token",
76+
session=requests.sessions.Session(),
77+
) == (
7078
"access_token",
7179
"refresh_token",
7280
)
@@ -89,7 +97,12 @@ def test_exchange_2fa_token_with_request_error(
8997
)
9098

9199
with pytest.raises(ApiException) as exc:
92-
exchange_2fa_token(self.api_host, "two_factor_token", "totp_token")
100+
exchange_2fa_token(
101+
self.api_host,
102+
"two_factor_token",
103+
"totp_token",
104+
session=requests.sessions.Session(),
105+
)
93106

94107
assert exc == ApiException(
95108
status=500, headers={"foo": "bar"}, body="Error body"
@@ -111,7 +124,12 @@ def test_refresh_access_token(self, mock_post_request, mock_response):
111124
"refresh_token": "refresh_token",
112125
}
113126

114-
assert refresh_access_token(self.api_host, "access_token", "refresh_token") == (
127+
assert refresh_access_token(
128+
self.api_host,
129+
"access_token",
130+
"refresh_token",
131+
session=requests.sessions.Session(),
132+
) == (
115133
"access_token",
116134
"refresh_token",
117135
)
@@ -138,7 +156,12 @@ def test_refresh_access_token_with_request_error(
138156
}
139157

140158
with pytest.raises(ApiException) as exc:
141-
exchange_2fa_token(self.api_host, "two_factor_token", "totp_token")
159+
exchange_2fa_token(
160+
self.api_host,
161+
"two_factor_token",
162+
"totp_token",
163+
session=requests.sessions.Session(),
164+
)
142165

143166
assert exc == ApiException(
144167
status=500, headers={"foo": "bar"}, body="Error body"

0 commit comments

Comments
 (0)