This is an automated email from the ASF dual-hosted git repository.

arm pushed a commit to branch jwtoken_multiple_sources
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/jwtoken_multiple_sources by 
this push:
     new d553c62  #504 - Add new ATR token to github workflows and validate
d553c62 is described below

commit d553c62cbff12fe6bf45eaa3166bae6746792473
Author: Alastair McFarlane <[email protected]>
AuthorDate: Tue Jan 27 10:18:58 2026 +0000

    #504 - Add new ATR token to github workflows and validate
---
 atr/api/__init__.py |  48 +++++------
 atr/jwtoken.py      | 225 +++++++++++++++++++++++++++++-----------------------
 atr/tasks/gha.py    |  11 ++-
 3 files changed, 157 insertions(+), 127 deletions(-)

diff --git a/atr/api/__init__.py b/atr/api/__init__.py
index 09eb198..ef9840e 100644
--- a/atr/api/__init__.py
+++ b/atr/api/__init__.py
@@ -253,7 +253,7 @@ async def committees_list() -> DictResponse:
 
 
 @api.route("/distribute/ssh/register", methods=["POST"])
[email protected](token_types=["github"])
[email protected](tp=True, atr=False)
 @quart_schema.validate_request(models.api.DistributeSshRegisterArgs)
 async def distribute_ssh_register(data: models.api.DistributeSshRegisterArgs) 
-> DictResponse:
     """
@@ -284,7 +284,7 @@ async def distribute_ssh_register(data: 
models.api.DistributeSshRegisterArgs) ->
 
 
 @api.route("/distribute/task/status", methods=["POST"])
[email protected](token_types=["github"])
[email protected](tp=True)
 @quart_schema.validate_request(models.api.DistributeStatusUpdateArgs)
 async def update_distribution_task_status(data: 
models.api.DistributeStatusUpdateArgs) -> DictResponse:
     """
@@ -308,7 +308,7 @@ async def update_distribution_task_status(data: 
models.api.DistributeStatusUpdat
 
 
 @api.route("/distribution/record", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True, tp=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.DistributionRecordArgs)
 @quart_schema.validate_response(models.api.DistributionRecordResults, 200)
@@ -347,7 +347,7 @@ async def distribution_record(data: 
models.api.DistributionRecordArgs) -> DictRe
 
 
 @api.route("/distribute/record_from_workflow", methods=["POST"])
[email protected](token_types=["github"])
[email protected](atr=False, tp=True)
 @quart_schema.validate_request(models.api.DistributionRecordFromWorkflowArgs)
 async def distribution_record_from_workflow(data: 
models.api.DistributionRecordFromWorkflowArgs) -> DictResponse:
     """
@@ -385,7 +385,7 @@ async def distribution_record_from_workflow(data: 
models.api.DistributionRecordF
 
 
 @api.route("/ignore/add", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.IgnoreAddArgs)
 @quart_schema.validate_response(models.api.IgnoreAddResults, 200)
@@ -414,7 +414,7 @@ async def ignore_add(data: models.api.IgnoreAddArgs) -> 
DictResponse:
 
 
 @api.route("/ignore/delete", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.IgnoreDeleteArgs)
 @quart_schema.validate_response(models.api.IgnoreDeleteResults, 200)
@@ -479,7 +479,7 @@ async def jwt_create(data: models.api.JwtCreateArgs) -> 
DictResponse:
 
 @api.route("/key/add", methods=["POST"])
 @rate_limiter.rate_limit(10, datetime.timedelta(hours=1))
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.KeyAddArgs)
 @quart_schema.validate_response(models.api.KeyAddResults, 200)
@@ -512,7 +512,7 @@ async def key_add(data: models.api.KeyAddArgs) -> 
DictResponse:
 
 
 @api.route("/key/delete", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.KeyDeleteArgs)
 @quart_schema.validate_response(models.api.KeyDeleteResults, 200)
@@ -566,7 +566,7 @@ async def key_get(fingerprint: str) -> DictResponse:
 
 @api.route("/keys/upload", methods=["POST"])
 @rate_limiter.rate_limit(10, datetime.timedelta(hours=1))
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.KeysUploadArgs)
 @quart_schema.validate_response(models.api.KeysUploadResults, 200)
@@ -721,7 +721,7 @@ async def projects_list() -> DictResponse:
 
 
 @api.route("/publisher/distribution/record", methods=["POST"])
[email protected](token_types=["github"])
[email protected](tp=True, atr=False)
 @quart_schema.validate_request(models.api.PublisherDistributionRecordArgs)
 async def publisher_distribution_record(data: 
models.api.PublisherDistributionRecordArgs) -> DictResponse:
     """
@@ -772,7 +772,7 @@ async def publisher_distribution_record(data: 
models.api.PublisherDistributionRe
 
 
 @api.route("/publisher/release/announce", methods=["POST"])
[email protected](token_types=["github"])
[email protected](tp=True, atr=False)
 @quart_schema.validate_request(models.api.PublisherReleaseAnnounceArgs)
 async def publisher_release_announce(data: 
models.api.PublisherReleaseAnnounceArgs) -> DictResponse:
     """
@@ -809,7 +809,7 @@ async def publisher_release_announce(data: 
models.api.PublisherReleaseAnnounceAr
 
 
 @api.route("/publisher/ssh/register", methods=["POST"])
[email protected](token_types=["github"])
[email protected](tp=True, atr=False)
 @rate_limiter.rate_limit(10, datetime.timedelta(hours=1))
 @quart_schema.validate_request(models.api.PublisherSshRegisterArgs)
 async def publisher_ssh_register(data: models.api.PublisherSshRegisterArgs) -> 
DictResponse:
@@ -840,7 +840,7 @@ async def publisher_ssh_register(data: 
models.api.PublisherSshRegisterArgs) -> D
 
 
 @api.route("/publisher/vote/resolve", methods=["POST"])
[email protected](token_types=["github"])
[email protected](tp=True, atr=False)
 @quart_schema.validate_request(models.api.PublisherVoteResolveArgs)
 async def publisher_vote_resolve(data: models.api.PublisherVoteResolveArgs) -> 
DictResponse:
     """
@@ -872,7 +872,7 @@ async def publisher_vote_resolve(data: 
models.api.PublisherVoteResolveArgs) -> D
 
 
 @api.route("/release/announce", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.ReleaseAnnounceArgs)
 @quart_schema.validate_response(models.api.ReleaseAnnounceResults, 201)
@@ -910,7 +910,7 @@ async def release_announce(data: 
models.api.ReleaseAnnounceArgs) -> DictResponse
 
 
 @api.route("/release/create", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.ReleaseCreateArgs)
 @quart_schema.validate_response(models.api.ReleaseCreateResults, 201)
@@ -937,7 +937,7 @@ async def release_create(data: 
models.api.ReleaseCreateArgs) -> DictResponse:
 
 # TODO: Duplicates the below
 @api.route("/release/delete", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.ReleaseDeleteArgs)
 @quart_schema.validate_response(models.api.ReleaseDeleteResults, 200)
@@ -1020,7 +1020,7 @@ async def release_revisions(project: str, version: str) 
-> DictResponse:
 
 
 @api.route("/release/upload", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.ReleaseUploadArgs)
 @quart_schema.validate_response(models.api.ReleaseUploadResults, 201)
@@ -1088,7 +1088,7 @@ async def releases_list(query_args: 
models.api.ReleasesListQuery) -> DictRespons
 
 
 @api.route("/signature/provenance", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.SignatureProvenanceArgs)
 @quart_schema.validate_response(models.api.SignatureProvenanceResults, 200)
@@ -1152,7 +1152,7 @@ async def signature_provenance(data: 
models.api.SignatureProvenanceArgs) -> Dict
 
 @api.route("/ssh-key/add", methods=["POST"])
 @rate_limiter.rate_limit(10, datetime.timedelta(hours=1))
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.SshKeyAddArgs)
 @quart_schema.validate_response(models.api.SshKeyAddResults, 201)
@@ -1174,7 +1174,7 @@ async def ssh_key_add(data: models.api.SshKeyAddArgs) -> 
DictResponse:
 
 @api.route("/ssh-key/delete", methods=["POST"])
 @rate_limiter.rate_limit(10, datetime.timedelta(hours=1))
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.SshKeyDeleteArgs)
 @quart_schema.validate_response(models.api.SshKeyDeleteResults, 201)
@@ -1253,7 +1253,7 @@ async def tasks_list(query_args: 
models.api.TasksListQuery) -> DictResponse:
 
 @api.route("/user/info")
 @rate_limiter.rate_limit(10, datetime.timedelta(hours=1))
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_response(models.api.UserInfoResults, 200)
 async def user_info() -> DictResponse:
@@ -1311,7 +1311,7 @@ async def users_list() -> DictResponse:
 
 # TODO: Add endpoints to allow users to vote
 @api.route("/vote/resolve", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.VoteResolveArgs)
 @quart_schema.validate_response(models.api.VoteResolveResults, 200)
@@ -1346,7 +1346,7 @@ async def vote_resolve(data: models.api.VoteResolveArgs) 
-> DictResponse:
 
 
 @api.route("/vote/start", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.VoteStartArgs)
 @quart_schema.validate_response(models.api.VoteStartResults, 201)
@@ -1389,7 +1389,7 @@ async def vote_start(data: models.api.VoteStartArgs) -> 
DictResponse:
 
 
 @api.route("/vote/tabulate", methods=["POST"])
[email protected](token_types=["atr"])
[email protected](atr=True)
 @quart_schema.security_scheme([{"BearerAuth": []}])
 @quart_schema.validate_request(models.api.VoteTabulateArgs)
 @quart_schema.validate_response(models.api.VoteTabulateResults, 200)
diff --git a/atr/jwtoken.py b/atr/jwtoken.py
index dc67ff8..428547c 100644
--- a/atr/jwtoken.py
+++ b/atr/jwtoken.py
@@ -34,23 +34,29 @@ import atr.log as log
 _ALGORITHM: Final[str] = "HS256"
 _ATR_JWT_AUDIENCE: Final[str] = "atr-api-pat-test-v1"
 _ATR_JWT_ISSUER: Final[str] = f"https://{config.get().APP_HOST}/"
-_GITHUB_OIDC_AUDIENCE: Final[str] = "atr-test-v1"
-_GITHUB_OIDC_EXPECTED: Final[dict[str, str]] = {
-    "enterprise": "the-asf",
-    "enterprise_id": "212555",
-    "repository_owner": "apache",
-    "runner_environment": "github-hosted",
-}
-_GITHUB_OIDC_ISSUER: Final[str] = "https://token.actions.githubusercontent.com";
-_GITHUB_TOKEN_FIELD: Final[str] = "jwt"
 _GITHUB_TRUSTED_ROLE_NID: Final[int] = 254436773
 _JWT_SECRET_KEY: Final[str] = config.get().JWT_SECRET_KEY
 
+# Trusted Publishers configuration
+_TRUSTED_PUBLISHERS: Final[dict[str, dict[str, Any]]] = {
+    "https://token.actions.githubusercontent.com": {
+        "name": "github",
+        "audience": "atr-test-v1",
+        "algorithm": "RS256",
+        "expected_claims": {
+            "enterprise": "the-asf",
+            "enterprise_id": "212555",
+            "repository_owner": "apache",
+            "runner_environment": "github-hosted",
+        },
+    },
+}
+
 if TYPE_CHECKING:
     from collections.abc import Awaitable, Callable, Coroutine
 
 
-def issue(uid: str, *, ttl: int = 90 * 60) -> str:
+def issue(uid: str, *, ttl: int = 90 * 60, claims: dict[str, str] | None = 
None) -> str:
     now = datetime.datetime.now(tz=datetime.UTC)
     payload = {
         "sub": uid,
@@ -60,31 +66,49 @@ def issue(uid: str, *, ttl: int = 90 * 60) -> str:
         "exp": now + datetime.timedelta(seconds=ttl),
         "jti": secrets.token_hex(128 // 8),
     }
+    if claims:
+        payload.update(claims)
     return jwt.encode(payload, _JWT_SECRET_KEY, algorithm=_ALGORITHM)
 
 
-def require[**P, R](*, token_types=None) -> Callable[[Callable[P, 
Coroutine[Any, Any, R]]], Callable[P, Awaitable[R]]]:
-    if token_types is None:
-        token_types = ["atr"]
+def require[**P, R](
+    *, atr: bool = False, tp: bool = False
+) -> Callable[[Callable[P, Coroutine[Any, Any, R]]], Callable[P, 
Awaitable[R]]]:
+    """
+    Require JWT authentication.
+
+    Args:
+        atr: Accept ATR tokens (default True)
+        tp: Accept Trusted Publisher tokens (default False)
+    """
 
     def decorator(func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, 
Awaitable[R]]:
         @functools.wraps(func)
         async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
-            errors: list[str] = []
-            claims = None
-            gh_asf_uid = None
-
-            for token_type in token_types:
-                claims, gh_asf_uid = await _try_verify_token(token_type, 
quart.request, errors)
-                if claims is not None:
-                    break
+            # Always extract token from Authorization header
+            token, secondary_token = _extract_bearer_tokens(quart.request)
+
+            # Decode without verification to check the issuer
+            try:
+                unverified = unverified_header_and_payload(token)
+                issuer = unverified["payload"].get("iss")
+            except Exception as exc:
+                raise base.ASFQuartException(f"Failed to decode token: {exc}", 
errorcode=401) from exc
+
+            if not issuer:
+                raise base.ASFQuartException("Token missing 'iss' claim", 
errorcode=401)
+
+            try:
+                claims, asf_uid = await _verify_tokens(issuer, token, 
secondary_token, atr, tp)
+                quart.g.jwt_claims = claims
+                quart.g.github_asf_uid = asf_uid
+            except jwt.ExpiredSignatureError as exc:
+                raise base.ASFQuartException("Token has expired", 
errorcode=401) from exc
+            except jwt.InvalidTokenError as exc:
+                raise base.ASFQuartException("Invalid JWT format", 
errorcode=401) from exc
+            except jwt.PyJWTError as exc:
+                raise base.ASFQuartException(f"Invalid JWT: {exc}", 
errorcode=401) from exc
 
-            if claims is None:
-                error_msg = "; ".join(errors) if errors else "Authentication 
required"
-                raise base.ASFQuartException(error_msg, errorcode=401)
-
-            quart.g.jwt_claims = claims
-            quart.g.github_asf_uid = gh_asf_uid
             return await func(*args, **kwargs)
 
         return wrapper
@@ -114,11 +138,12 @@ def verify(token: str) -> dict[str, Any]:
     )
 
 
-async def verify_github_oidc(token: str) -> dict[str, Any]:
+async def _verify_oidc_token(token: str, issuer: str, audience: str, 
algorithm: str) -> dict[str, Any]:
+    """Verify an OIDC token using JWKS."""
     try:
         async with aiohttp.ClientSession() as session:
             r = await session.get(
-                f"{_GITHUB_OIDC_ISSUER}/.well-known/openid-configuration",
+                f"{issuer}/.well-known/openid-configuration",
                 timeout=aiohttp.ClientTimeout(total=10),
             )
             r.raise_for_status()
@@ -126,106 +151,104 @@ async def verify_github_oidc(token: str) -> dict[str, 
Any]:
     except aiohttp.ClientSSLError as exc:
         log.error(f"TLS failure fetching OIDC config: {exc}")
         raise base.ASFQuartException(
-            f"TLS verification failed for GitHub OIDC endpoint: {exc}",
+            f"TLS verification failed for OIDC endpoint: {exc}",
             errorcode=502,
         ) from exc
     except aiohttp.ClientConnectionError as exc:
-        log.error(f"Failed to connect to GitHub OIDC endpoint: {exc}")
+        log.error(f"Failed to connect to OIDC endpoint: {exc}")
         raise base.ASFQuartException(
-            f"Failed to connect to GitHub OIDC endpoint: {exc}",
+            f"Failed to connect to OIDC endpoint: {exc}",
             errorcode=502,
         ) from exc
     except aiohttp.ClientResponseError as exc:
-        log.error(f"GitHub OIDC endpoint returned HTTP {exc.status}: 
{exc.message}")
+        log.error(f"OIDC endpoint returned HTTP {exc.status}: {exc.message}")
         raise base.ASFQuartException(
-            f"GitHub OIDC endpoint returned HTTP {exc.status}: {exc.message}",
+            f"OIDC endpoint returned HTTP {exc.status}: {exc.message}",
             errorcode=502,
         ) from exc
     except (aiohttp.ServerTimeoutError, aiohttp.ClientError) as exc:
         log.warning(f"Failed to fetch OIDC config: {exc}")
-        jwks_uri = f"{_GITHUB_OIDC_ISSUER}/.well-known/jwks"
+        jwks_uri = f"{issuer}/.well-known/jwks"
 
     jwks_client = jwt.PyJWKClient(jwks_uri)
     signing_key = jwks_client.get_signing_key_from_jwt(token)
     payload = jwt.decode(
         token,
         key=signing_key.key,
-        algorithms=["RS256"],
-        audience=_GITHUB_OIDC_AUDIENCE,
-        issuer=_GITHUB_OIDC_ISSUER,
+        algorithms=[algorithm],
+        audience=audience,
+        issuer=issuer,
         options={"require": ["exp", "iat"]},
     )
-    for key, value in _GITHUB_OIDC_EXPECTED.items():
-        if payload[key] != value:
-            raise base.ASFQuartException(
-                f"GitHub OIDC payload mismatch: {key} = {payload[key]} != 
{value}",
-                errorcode=401,
-            )
-    # del payload["actor_id"]
-    del payload["repository_id"]
-    del payload["repository_owner_id"]
-    del payload["run_id"]
     return payload
 
 
-def _extract_bearer_token(request: quart.Request) -> str:
-    header = request.headers.get("Authorization", "")
-    scheme, _, token = header.partition(" ")
-    if (scheme.lower() != "bearer") or (not token):
-        raise base.ASFQuartException(
-            "Authentication required. Please provide a valid Bearer token in 
the Authorization header", errorcode=401
-        )
-    return token
+async def _verify_tokens(
+    issuer, token: str, secondary_token: str, atr: bool, tp: bool
+) -> tuple[dict[str, Any], str | None]:
+    """Route to appropriate verifier based on issuer and verify tokens"""
+    if issuer == _ATR_JWT_ISSUER:
+        if not atr:
+            raise base.ASFQuartException("ATR tokens not accepted for this 
endpoint", errorcode=401)
+        claims = verify(token)
+        return claims, None
+    elif issuer in _TRUSTED_PUBLISHERS:
+        if not tp:
+            raise base.ASFQuartException("Trusted Publisher tokens not 
accepted for this endpoint", errorcode=401)
+        claims, asf_uid = await _verify_trusted_publisher(token, issuer, 
secondary_token)
+        return claims, asf_uid
+    else:
+        raise base.ASFQuartException(f"Unknown token issuer: {issuer}", 
errorcode=401)
 
 
-async def _extract_token_from_body(request: quart.Request, field: str) -> 
tuple[str, dict[str, Any]]:
-    try:
-        body = await request.get_json()
-    except Exception as exc:
-        raise base.ASFQuartException("Invalid JSON in request body", 
errorcode=400) from exc
+async def _verify_trusted_publisher(token: str, issuer: str, secondary_token: 
str) -> tuple[dict[str, Any], str | None]:
+    """Verify a Trusted Publisher token and return claims and ASF UID."""
+    publisher_config = _TRUSTED_PUBLISHERS[issuer]
 
-    if not body:
-        raise base.ASFQuartException("Request body is required", errorcode=400)
+    # Verify the token using OIDC
+    payload = await _verify_oidc_token(token, issuer, 
publisher_config["audience"], publisher_config["algorithm"])
 
-    token = body.get(field)
-    if not token:
-        raise base.ASFQuartException(f"Missing '{field}' field in request 
body", errorcode=400)
+    # Check expected claims
+    for key, value in publisher_config["expected_claims"].items():
+        if payload.get(key) != value:
+            raise base.ASFQuartException(
+                f"Trusted Publisher payload mismatch: {key} = 
{payload.get(key)} != {value}",
+                errorcode=401,
+            )
 
-    if not isinstance(token, str):
-        raise base.ASFQuartException(f"'{field}' must be a string", 
errorcode=400)
+    # Clean up unnecessary fields
+    payload.pop("repository_id", None)
+    payload.pop("repository_owner_id", None)
+    payload.pop("run_id", None)
+
+    # Determine ASF UID
+    # If actor_id is the trusted role, check for a secondary token to find the 
ASF UID
+    # Otherwise, look up the ASF UID from GitHub ID
+    actor_id = payload.get("actor_id")
+    if actor_id and int(actor_id) != _GITHUB_TRUSTED_ROLE_NID:
+        asf_uid = await ldap.github_to_apache(actor_id)
+    else:
+        if secondary_token and len(secondary_token) > 0:
+            asf_uid = _validate_workflow_token(secondary_token, 
payload["workflow_ref"])
+        else:
+            asf_uid = None
 
-    return token, body
+    return payload, asf_uid
 
 
-async def _try_verify_token(
-    token_type: str, request: quart.Request, errors: list[str]
-) -> tuple[dict[str, Any] | None, str | None] | tuple[None, None]:
-    try:
-        if token_type == "atr":
-            token = _extract_bearer_token(request)
-            return verify(token), None
-        if token_type == "github":
-            token, body = await _extract_token_from_body(request, 
_GITHUB_TOKEN_FIELD)
-            github_payload, asf_uid = await 
_validate_trusted_jwt(body.get("publisher", ""), token)
-            return github_payload, asf_uid
-        raise RuntimeError(f"Invalid token type: {token_type}")
-    except jwt.ExpiredSignatureError:
-        errors.append(f"{token_type}: Token has expired")
-    except jwt.InvalidTokenError:
-        errors.append(f"{token_type}: Invalid Bearer JWT format")
-    except jwt.PyJWTError as exc:
-        errors.append(f"{token_type}: Invalid Bearer JWT: {exc!s}")
-    except Exception as exc:
-        errors.append(f"{token_type}: {exc!s}")
-    return None, None
-
-
-async def _validate_trusted_jwt(publisher: str, token: str) -> tuple[dict[str, 
Any], str | None]:
-    if publisher != "github":
-        raise jwt.InvalidTokenError(f"Publisher {publisher} not supported")
-    payload = await verify_github_oidc(token)
-    if int(payload["actor_id"]) != _GITHUB_TRUSTED_ROLE_NID:
-        asf_uid = await ldap.github_to_apache(payload["actor_id"])
-    else:
-        asf_uid = None
-    return payload, asf_uid
+def _validate_workflow_token(token: str, workflow: str) -> str:
+    payload = verify(token)
+    if payload.get("workflow_ref", "") != workflow:
+        raise base.ASFQuartException("Invalid workflow token", errorcode=401)
+    return payload["sub"]
+
+
+def _extract_bearer_tokens(request: quart.Request) -> tuple[str, str]:
+    header = request.headers.get("Authorization", "")
+    secondary_token = request.headers.get("ATR-Auth", "")
+    scheme, _, token = header.partition(" ")
+    if (scheme.lower() != "bearer") or (not token):
+        raise base.ASFQuartException(
+            "Authentication required. Please provide a valid Bearer token in 
the Authorization header", errorcode=401
+        )
+    return token, secondary_token
diff --git a/atr/tasks/gha.py b/atr/tasks/gha.py
index 5e3a628..0776709 100644
--- a/atr/tasks/gha.py
+++ b/atr/tasks/gha.py
@@ -26,6 +26,7 @@ import pydantic
 
 import atr.config as config
 import atr.db as db
+import atr.jwtoken as jwtoken
 import atr.log as log
 import atr.models.results as results
 import atr.models.schema as schema
@@ -75,11 +76,17 @@ async def trigger_workflow(args: DistributionWorkflow, *, 
task_id: int | None =
     except KeyError:
         _fail(f"Invalid platform: {args.platform}")
     workflow = f"distribute-{sql_platform.value.gh_slug}{'-stg' if 
args.staging else ''}.yml"
+    ref = "main"
+    token = jwtoken.issue(
+        args.asf_uid,
+        ttl=60 * 60,
+        claims={"atr-workflow": 
f"apache/tooling-actions/.github/workflows/{workflow}@refs/heads/{ref}"},
+    )
     payload = {
-        "ref": "main",
+        "ref": ref,
         "inputs": {
             "atr-id": unique_id,
-            "asf-uid": args.asf_uid,
+            "atr-token": token,
             "project": args.project_name,
             "phase": args.phase,
             "version": args.version_name,


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to