You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2025/08/05 13:32:35 UTC
(tooling-trusted-release) branch main updated: Track last time of use for PATs
This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git
The following commit(s) were added to refs/heads/main by this push:
new 9f1641a Track last time of use for PATs
9f1641a is described below
commit 9f1641a8cae8cf0205506334bcabba12987e00c5
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Tue Aug 5 14:31:59 2025 +0100
Track last time of use for PATs
---
atr/blueprints/api/api.py | 14 +++++---------
atr/routes/tokens.py | 13 +++++++------
atr/storage/writers/tokens.py | 31 ++++++++++++++++++++++++++-----
3 files changed, 38 insertions(+), 20 deletions(-)
diff --git a/atr/blueprints/api/api.py b/atr/blueprints/api/api.py
index b501c20..a5eae63 100644
--- a/atr/blueprints/api/api.py
+++ b/atr/blueprints/api/api.py
@@ -17,7 +17,6 @@
import base64
-import datetime
import hashlib
import pathlib
from typing import Any
@@ -337,18 +336,15 @@ async def jwt_create(data: models.api.JwtCreateArgs) -> DictResponse:
"""
# Expects {"asfuid": "uid", "pat": "pat-token"}
# Returns {"asfuid": "uid", "jwt": "jwt-token"}
- token_hash = hashlib.sha3_256(data.pat.encode()).hexdigest()
- pat_rec = await _get_pat(data.asfuid, token_hash)
-
- now = datetime.datetime.now(datetime.UTC)
- if (pat_rec is None) or (pat_rec.expires < now):
- raise exceptions.Unauthorized("Invalid PAT")
+ asf_uid = data.asfuid
+ async with storage.write(asf_uid) as write:
+ wafc = write.as_foundation_committer()
+ jwt = await wafc.tokens.issue_jwt(data.pat)
- jwt_token = jwtoken.issue(data.asfuid)
return models.api.JwtCreateResults(
endpoint="/jwt/create",
asfuid=data.asfuid,
- jwt=jwt_token,
+ jwt=jwt,
).model_dump(), 200
diff --git a/atr/routes/tokens.py b/atr/routes/tokens.py
index 89b38e7..e287a93 100644
--- a/atr/routes/tokens.py
+++ b/atr/routes/tokens.py
@@ -108,9 +108,10 @@ async def tokens(session: routes.CommitterSession) -> str | response.Response:
issue_jwt_elem = div[
p[
- "Generate a JSON Web Token (JWT) to authenticate calls to ATR's private API routes. "
- "Treat the token like a password and include it in the Authorization header "
- "as a Bearer token when invoking the protected endpoints.",
+ """Generate a JSON Web Token (JWT) to authenticate calls to ATR's
+ private API routes. Treat the token like a password and include it
+ in the Authorization header as a Bearer token when invoking the
+ protected endpoints."""
# p["Example"],
],
issue_form_elem,
@@ -121,9 +122,9 @@ async def tokens(session: routes.CommitterSession) -> str | response.Response:
h1["Tokens"],
h2["Personal Access Tokens (PATs)"],
p[
- "Generate tokens for API access. For security, the plaintext token "
- "is shown only once when you create it. You can revoke tokens you no "
- "longer need.",
+ """Generate tokens for API access. For security, the plaintext
+ token is shown only once when you create it. You can revoke tokens
+ you no longer need."""
],
div(".card.mb-4")[
div(".card-header")["Generate new token"],
diff --git a/atr/storage/writers/tokens.py b/atr/storage/writers/tokens.py
index 72e49d4..e503386 100644
--- a/atr/storage/writers/tokens.py
+++ b/atr/storage/writers/tokens.py
@@ -18,15 +18,16 @@
# Removing this will cause circular imports
from __future__ import annotations
-from typing import TYPE_CHECKING
+import datetime
+import hashlib
+
+import sqlmodel
import atr.db as db
+import atr.jwtoken as jwtoken
import atr.models.sql as sql
import atr.storage as storage
-if TYPE_CHECKING:
- import datetime
-
class GeneralPublic:
def __init__(
@@ -47,7 +48,10 @@ class FoundationCommitter(GeneralPublic):
self.__credentials = credentials
self.__write = write
self.__data = data
- self.__asf_uid = write.authorisation.asf_uid
+ asf_uid = write.authorisation.asf_uid
+ if asf_uid is None:
+ raise storage.AccessError("No ASF UID")
+ self.__asf_uid = asf_uid
async def add_token(
self, uid: str, token_hash: str, created: datetime.datetime, expires: datetime.datetime, label: str | None
@@ -63,6 +67,23 @@ class FoundationCommitter(GeneralPublic):
await self.__data.commit()
return pat
+ async def issue_jwt(self, pat_text: str) -> str:
+ pat_hash = hashlib.sha3_256(pat_text.encode()).hexdigest()
+ pat = await self.__data.query_one_or_none(
+ sqlmodel.select(sql.PersonalAccessToken).where(
+ sql.PersonalAccessToken.asfuid == self.__asf_uid,
+ sql.PersonalAccessToken.token_hash == pat_hash,
+ )
+ )
+ if pat is None:
+ raise storage.AccessError("Invalid PAT")
+ if pat.expires < datetime.datetime.now(datetime.UTC):
+ raise storage.AccessError("Expired PAT")
+ issued_jwt = jwtoken.issue(self.__asf_uid)
+ pat.last_used = datetime.datetime.now(datetime.UTC)
+ await self.__data.commit()
+ return issued_jwt
+
class CommitteeParticipant(FoundationCommitter):
def __init__(
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org