Skip to content

Commit 744e7d6

Browse files
authored
Rely solely on anaconda_auth for a/ token (#189)
* require anaconda_auth for a/ token * cleanup
1 parent 3ec6191 commit 744e7d6

3 files changed

Lines changed: 77 additions & 110 deletions

File tree

anaconda_anon_usage/tokens.py

Lines changed: 23 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# child environments.
66

77
import base64
8+
import datetime as dt
89
import json
910
import re
1011
import sys
@@ -208,7 +209,7 @@ def _jwt_to_token(s):
208209
exp: the expiration time
209210
"""
210211
if not s:
211-
return None, 0
212+
return
212213
try:
213214
# The JWT should have three parts separated by periods
214215
parts = s.split(".")
@@ -223,64 +224,44 @@ def _jwt_to_token(s):
223224
# The payload should have a positive integer expiration
224225
exp = parts[1].get("exp")
225226
assert isinstance(exp, int) and exp > 0, "Invalid expiration"
227+
now = dt.datetime.now(tz=dt.timezone.utc).timestamp()
228+
if exp < now:
229+
_debug("API key expired %ds ago", int(now - exp))
230+
return
226231
# The subscriber should be a non-empty UUID string
227232
sub = parts[1].get("sub")
228233
assert sub, "Invalid subscriber"
229234
# This is an Anaconda requirement, not a JWT requirement
230235
sub = uuid.UUID(sub).bytes
231236
token = base64.urlsafe_b64encode(sub).decode("ascii").strip("=")
232-
return token, exp
237+
return token
233238
except Exception as exc:
234239
_debug("Unexpected %s parsing API key: %s", type(exc), exc)
235-
return None, 0
236240

237241

238242
@cached
239243
def anaconda_auth_token():
240-
"""Retrieve Anaconda Cloud token from keyring.
241-
242-
Examines all entries under 'Anaconda Cloud' in the keyring and
243-
selects the token with the latest expiration date. This handles
244-
migration from 'anaconda.cloud' to 'anaconda.com' entries.
245-
244+
"""Returns the base64-encoded uid corresponding to the logged
245+
in Anaconda Cloud user, if one is present.
246246
Returns:
247247
str: Base64-encoded token, or None if no valid token found.
248248
"""
249-
env = environ.get("ANACONDA_AUTH_API_KEY")
250-
if env:
251-
_debug("ANACONDA_AUTH_API_KEY environment variable found")
252-
token, _ = _jwt_to_token(env)
253-
if token:
254-
_debug("Retrieved Anaconda token from environment: %s", token)
255-
return token
256-
_debug("Could not retrieve API key from environment")
257-
fpath = expanduser(join(ANACONDA_DIR, "keyring"))
258-
data = _read_file(fpath, "anaconda keyring")
259-
if not data:
260-
return
261249
try:
262-
data = json.loads(data)
250+
from anaconda_auth.token import TokenInfo, TokenNotFoundError
251+
252+
_debug("Module anaconda_auth loaded")
253+
tinfo = TokenInfo.load(domain="anaconda.com")
254+
if tinfo.api_key:
255+
token = _jwt_to_token(tinfo.api_key)
256+
_debug("Retrieved Anaconda auth token: %s", token)
257+
return token
258+
except ImportError:
259+
_debug("Module anaconda_auth not available")
260+
except TokenNotFoundError:
261+
pass
263262
except Exception as exc:
264-
_debug("Unexpected JSON decoding error parsing keyring file: %s", exc)
265-
return
266-
if not data or not isinstance(data, dict):
267-
_debug("Empty keyring")
268-
return
269-
token, exp = None, 0
270-
for key, rec in (data.get("Anaconda Cloud") or {}).items():
271-
try:
272-
tdata = json.loads(base64.b64decode(rec))["api_key"]
273-
except Exception as exc:
274-
_debug("Unexpected error parsing keyring entry '%s': %s", key, exc)
275-
t_token, t_exp = _jwt_to_token(tdata)
276-
if t_exp > exp:
277-
token = t_token
278-
exp = t_exp
279-
if token:
280-
_debug("Retrieved Anaconda token from keyring: %s", token)
281-
else:
282-
_debug("No Anaconda keyring records found")
283-
return token
263+
_debug("Unexpected error retrieving token using anaconda_auth: %s", exc)
264+
_debug("No Anaconda API token found")
284265

285266

286267
@cached

tests/conftest.py

Lines changed: 32 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import base64
2+
import datetime as dt
23
import json
34
import tempfile
45
import uuid
5-
from os import environ, mkdir
6+
from os import mkdir
67
from os.path import dirname, join
78

89
import pytest
@@ -11,57 +12,57 @@
1112

1213
from anaconda_anon_usage import tokens, utils
1314

15+
try:
16+
from anaconda_auth.token import AnacondaKeyring
17+
except ImportError:
18+
AnacondaKeyring = None
1419

15-
def _jsond(rec, urlsafe=False):
16-
return (
17-
base64.urlsafe_b64encode(json.dumps(rec).encode("ascii"))
18-
.decode("ascii")
19-
.rstrip("=")
20-
)
2120

21+
def _jsond(rec, strip=True):
22+
result = json.dumps(rec)
23+
result = base64.urlsafe_b64encode(result.encode("ascii"))
24+
result = result.decode("ascii")
25+
if strip:
26+
result = result.rstrip("=")
27+
return result
2228

23-
def _test_keyring():
29+
30+
def _keyring_data():
2431
domains = ["random.domain", "anaconda.cloud", "anaconda.com"]
2532
drecs, exp = {}, 0
33+
exp = int(dt.datetime.now(tz=dt.timezone.utc).timestamp()) + 7884000
2634
for dom in domains:
27-
exp = exp + 123456
35+
exp = exp - 1
2836
sub = str(uuid.uuid4())
2937
header = {"alg": "RS256", "typ": "JWT"}
3038
payload = {"exp": exp, "sub": sub}
3139
# Not a real signature but we just need it to be a base64-encoded blob
3240
signature = {"fake": dom}
3341
api_key = ".".join(map(_jsond, (header, payload, signature)))
3442
rec = {"domain": dom, "api_key": api_key, "repo_tokens": [], "version": 2}
35-
drecs[dom] = _jsond(rec)
43+
# anaconda_auth doesn't like it when we strip the padding
44+
drecs[dom] = _jsond(rec, strip=False)
3645
result = {"Anaconda Cloud": drecs}
3746
return json.dumps(result), sub, api_key
3847

3948

4049
@pytest.fixture
41-
def aau_token_path():
42-
old_dir, old_adir = tokens.CONFIG_DIR, tokens.ANACONDA_DIR
43-
with tempfile.TemporaryDirectory() as tname:
44-
tokens.CONFIG_DIR = tokens.ANACONDA_DIR = tname
45-
yield join(tname, "aau_token")
46-
tokens.CONFIG_DIR = old_dir
47-
tokens.ANACONDA_DIR = old_adir
48-
49-
50-
@pytest.fixture()
51-
def anaconda_uid(aau_token_path):
52-
kpath = join(dirname(aau_token_path), "keyring")
53-
kstr, sub, _ = _test_keyring()
54-
with open(kpath, "w") as fp:
50+
def api_key_sub(monkeypatch, aau_token_path):
51+
kstr, sub, _ = _keyring_data()
52+
keyring_path = join(dirname(aau_token_path), "keyring")
53+
with open(keyring_path, "w") as fp:
5554
fp.write(kstr)
56-
yield sub
55+
return sub
5756

5857

59-
@pytest.fixture()
60-
def anaconda_uid_env():
61-
_, sub, api_key = _test_keyring()
62-
environ["ANACONDA_AUTH_API_KEY"] = api_key
63-
yield sub
64-
del environ["ANACONDA_AUTH_API_KEY"]
58+
@pytest.fixture
59+
def aau_token_path(monkeypatch, tmp_path):
60+
monkeypatch.setattr(tokens, "CONFIG_DIR", str(tmp_path))
61+
monkeypatch.setattr(tokens, "ANACONDA_DIR", str(tmp_path))
62+
keyring_path = tmp_path / "keyring"
63+
if AnacondaKeyring is not None:
64+
monkeypatch.setattr(AnacondaKeyring, "keyring_path", keyring_path)
65+
return str(tmp_path / "aau_token")
6566

6667

6768
def _system_token_path(npaths=1):
@@ -126,22 +127,9 @@ def two_dotted_org_tokens(aau_token_path):
126127
yield t1 + t2[:1]
127128

128129

129-
def _env_clear():
130-
if "ANACONDA_AUTH_API_KEY" in environ:
131-
del environ["ANACONDA_AUTH_API_KEY"]
132-
if "ANACONDA_ANON_USAGE_ORG_TOKEN" in environ:
133-
del environ["ANACONDA_ANON_USAGE_ORG_TOKEN"]
134-
if "ANACONDA_ANON_USAGE_MACHINE_TOKEN" in environ:
135-
del environ["ANACONDA_ANON_USAGE_MACHINE_TOKEN"]
136-
if "ANACONDA_ANON_USAGE_INSTALLER_TOKEN" in environ:
137-
del environ["ANACONDA_ANON_USAGE_INSTALLER_TOKEN"]
138-
139-
140130
@pytest.fixture(autouse=True)
141131
def client_token_string_cache_cleanup(request):
142-
_env_clear()
143132
request.addfinalizer(utils._cache_clear)
144-
request.addfinalizer(_env_clear)
145133

146134

147135
@pytest.fixture(autouse=True)

tests/unit/test_tokens.py

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import base64
22
import re
33
import uuid
4-
from os import environ
54
from os.path import exists
65

6+
import pytest
7+
78
from anaconda_anon_usage import tokens, utils
89

10+
try:
11+
import anaconda_auth
12+
except ImportError:
13+
anaconda_auth = None
14+
915

1016
def test_client_token(aau_token_path):
1117
assert not exists(aau_token_path)
@@ -150,26 +156,26 @@ def test_token_string_with_two_dotted_org_tokens(two_dotted_org_tokens):
150156
assert token_string.count(" o/") == 2
151157

152158

153-
def test_token_string_with_env_org_token(no_system_tokens):
159+
def test_token_string_with_env_org_token(monkeypatch, no_system_tokens):
154160
org_token_e = utils._random_token()
155161
mch_token_e = utils._random_token()
156162
ins_token_e = utils._random_token()
157-
environ["ANACONDA_ANON_USAGE_ORG_TOKEN"] = org_token_e
158-
environ["ANACONDA_ANON_USAGE_MACHINE_TOKEN"] = mch_token_e
159-
environ["ANACONDA_ANON_USAGE_INSTALLER_TOKEN"] = ins_token_e
163+
monkeypatch.setenv("ANACONDA_ANON_USAGE_ORG_TOKEN", org_token_e)
164+
monkeypatch.setenv("ANACONDA_ANON_USAGE_MACHINE_TOKEN", mch_token_e)
165+
monkeypatch.setenv("ANACONDA_ANON_USAGE_INSTALLER_TOKEN", ins_token_e)
160166
token_string = tokens.token_string()
161167
assert "o/" + org_token_e in token_string
162168
assert "m/" + mch_token_e in token_string
163169

164170

165-
def test_token_string_with_system_and_env(system_tokens):
171+
def test_token_string_with_system_and_env(monkeypatch, system_tokens):
166172
org_token, mch_token, ins_token = system_tokens
167173
org_token_e = utils._random_token()
168174
mch_token_e = utils._random_token()
169175
ins_token_e = utils._random_token()
170-
environ["ANACONDA_ANON_USAGE_ORG_TOKEN"] = org_token_e
171-
environ["ANACONDA_ANON_USAGE_MACHINE_TOKEN"] = mch_token_e
172-
environ["ANACONDA_ANON_USAGE_INSTALLER_TOKEN"] = ins_token_e
176+
monkeypatch.setenv("ANACONDA_ANON_USAGE_ORG_TOKEN", org_token_e)
177+
monkeypatch.setenv("ANACONDA_ANON_USAGE_MACHINE_TOKEN", mch_token_e)
178+
monkeypatch.setenv("ANACONDA_ANON_USAGE_INSTALLER_TOKEN", ins_token_e)
173179
token_string = tokens.token_string()
174180
assert "i/" + ins_token in token_string
175181
assert "i/" + ins_token_e in token_string
@@ -183,13 +189,13 @@ def test_token_string_with_system_and_env(system_tokens):
183189
assert token_string.count(" i/") == 2
184190

185191

186-
def test_token_string_with_invalid_tokens(no_system_tokens):
192+
def test_token_string_with_invalid_tokens(monkeypatch, no_system_tokens):
187193
org_token_e = "invalid token"
188194
mch_token_e = "superlongtokenthathasnobusinessbeinganactualtoken"
189195
ins_token_e = "fake installer"
190-
environ["ANACONDA_ANON_USAGE_ORG_TOKEN"] = org_token_e
191-
environ["ANACONDA_ANON_USAGE_MACHINE_TOKEN"] = mch_token_e
192-
environ["ANACONDA_ANON_USAGE_MACHINE_TOKEN"] = ins_token_e
196+
monkeypatch.setenv("ANACONDA_ANON_USAGE_ORG_TOKEN", org_token_e)
197+
monkeypatch.setenv("ANACONDA_ANON_USAGE_MACHINE_TOKEN", mch_token_e)
198+
monkeypatch.setenv("ANACONDA_ANON_USAGE_INSTALLER_TOKEN", ins_token_e)
193199
token_string = tokens.token_string()
194200
assert "o/" not in token_string
195201
assert "m/" not in token_string
@@ -249,19 +255,11 @@ def test_token_string_env_readonly(monkeypatch, no_system_tokens):
249255
assert "m/" not in token_string
250256

251257

252-
def test_anaconda_string_keyring(anaconda_uid):
253-
token_string = tokens.token_string()
254-
assert "a/" in token_string
255-
expected = uuid.UUID(anaconda_uid).bytes
256-
expected = base64.urlsafe_b64encode(expected).decode("ascii").rstrip("=")
257-
aval = re.sub("^.*a/", "", token_string).split(" ", 1)[0]
258-
assert aval == expected
259-
260-
261-
def test_anaconda_string_env(anaconda_uid_env):
258+
@pytest.mark.skipif(anaconda_auth is None, reason="Requires the anaconda_auth module")
259+
def test_keyring_in_module(api_key_sub):
262260
token_string = tokens.token_string()
263261
assert "a/" in token_string
264-
expected = uuid.UUID(anaconda_uid_env).bytes
262+
expected = uuid.UUID(api_key_sub).bytes
265263
expected = base64.urlsafe_b64encode(expected).decode("ascii").rstrip("=")
266264
aval = re.sub("^.*a/", "", token_string).split(" ", 1)[0]
267265
assert aval == expected

0 commit comments

Comments
 (0)