Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ local_resource('devstack-ensure', cmd='./devstack/devstack ensure', labels=['pla
# (encrypted with the committed keystore). It must NOT be regenerated, or the seed would no
# longer decrypt to it.

local_resource('build', cmd='mvn install -DskipTests -Dmaven.test.skip=true -T4 -q',
local_resource('build', cmd='mvn clean install -DskipTests -Dmaven.test.skip=true -T4 -q',
deps=['airavata-server/pom.xml'], labels=['build'])

docker_build('airavata-server:dev', '.', dockerfile='Dockerfile',
Expand Down
6 changes: 0 additions & 6 deletions airavata-python-sdk/airavata_experiments/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,9 @@ def save(self) -> None:
av = AiravataOperator(AuthContext.get_access_token())
az = av.__airavata_token__(av.access_token, av.default_gateway_id())
assert az.access_token is not None
assert az.claims_map is not None
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + az.access_token,
'X-Claims': json.dumps(dict(az.claims_map))
}
import requests
if self.id is None:
Expand Down Expand Up @@ -168,11 +166,9 @@ def load(id: str | None) -> Plan:
av = AiravataOperator(AuthContext.get_access_token())
az = av.__airavata_token__(av.access_token, av.default_gateway_id())
assert az.access_token is not None
assert az.claims_map is not None
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + az.access_token,
'X-Claims': json.dumps(dict(az.claims_map))
}
import requests
response = requests.get(f"{settings.API_SERVER_URL}/api/v1/plan/{id}", headers=headers)
Expand All @@ -189,11 +185,9 @@ def query() -> list[Plan]:
av = AiravataOperator(AuthContext.get_access_token())
az = av.__airavata_token__(av.access_token, av.default_gateway_id())
assert az.access_token is not None
assert az.claims_map is not None
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + az.access_token,
'X-Claims': json.dumps(dict(az.claims_map))
}
import requests
response = requests.get(f"{settings.API_SERVER_URL}/api/v1/plan/user", headers=headers)
Expand Down
23 changes: 0 additions & 23 deletions airavata-python-sdk/airavata_jupyter_magic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,16 +430,9 @@ def generate_headers(access_token: str, gateway_id: str) -> dict:
@returns: the headers

"""
decode = jwt.decode(access_token, options={"verify_signature": False})
user_id = decode['preferred_username']
claimsMap = {
"userName": user_id,
"gatewayID": gateway_id
}
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token,
'X-Claims': json.dumps(claimsMap)
}


Expand Down Expand Up @@ -682,18 +675,10 @@ def restart_runtime_kernel(access_token: str, rt_name: str, env_name: str, runti
settings = Settings()
url = f"{settings.API_SERVER_URL}/api/v1/agent/setup/restart"

decode = jwt.decode(access_token, options={"verify_signature": False})
user_id = decode['preferred_username']
claimsMap = {
"userName": user_id,
"gatewayID": runtime.gateway_id
}

# Headers
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token,
'X-Claims': json.dumps(claimsMap)
}

# Send the POST request
Expand Down Expand Up @@ -732,18 +717,10 @@ def stop_agent_job(access_token: str, runtime_name: str, runtime: RuntimeInfo):
settings = Settings()
url = f"{settings.API_SERVER_URL}/api/v1/exp/terminate/{runtime.experimentId}"

decode = jwt.decode(access_token, options={"verify_signature": False})
user_id = decode['preferred_username']
claimsMap = {
"userName": user_id,
"gatewayID": runtime.gateway_id
}

# Headers
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token,
'X-Claims': json.dumps(claimsMap)
}

# Send the POST request
Expand Down
28 changes: 22 additions & 6 deletions airavata-python-sdk/airavata_sdk/client.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import base64
import json
import logging
from typing import Optional

import grpc

log = logging.getLogger(__name__)


def _decode_jwt_claims(token):
"""Decode a JWT payload (no signature verification) to read identity claims for convenience.

Used only to expose ``username``/``gateway_id`` locally; authorization is the token itself,
which the server verifies and from which it derives identity.
"""
try:
payload = token.split(".")[1]
payload += "=" * (-len(payload) % 4)
return json.loads(base64.urlsafe_b64decode(payload))
except Exception:
return {}


class AiravataClient:
"""Transport-agnostic facade over all Airavata gRPC services.

Expand All @@ -24,13 +38,15 @@ def __init__(self, host, port, token, gateway_id, secure=False, claims=None):
else:
self._channel = grpc.insecure_channel(target, options=options)

self.claims = claims or {}
# Auth is the Keycloak access token and nothing else: it is sent as a Bearer
# credential and the server derives identity (user + gateway) from the verified
# token. Client-asserted identity (x-claims) is no longer sent.
self._token_claims = _decode_jwt_claims(token) if token else {}
self.claims = self._token_claims

self._metadata = []
if token:
self._metadata.append(("authorization", f"Bearer {token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._gateway_id = gateway_id

Expand All @@ -49,8 +65,8 @@ def __init__(self, host, port, token, gateway_id, secure=False, claims=None):

@property
def username(self):
"""Return the caller's username from JWT claims, or None if unavailable."""
return (self.claims or {}).get("userName")
"""Return the caller's username decoded from the access token, or None."""
return self._token_claims.get("preferred_username")

@property
def gateway_id(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_agent_interaction_service_stub(self.channel)

Expand Down
2 changes: 0 additions & 2 deletions airavata-python-sdk/airavata_sdk/clients/api_server_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

# Create all service stubs
self._experiment = create_experiment_service_stub(self.channel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_credential_service_stub(self.channel)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_experiment_management_service_stub(self.channel)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_group_manager_service_stub(self.channel)

Expand Down
2 changes: 0 additions & 2 deletions airavata-python-sdk/airavata_sdk/clients/iam_admin_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_iam_admin_service_stub(self.channel)

Expand Down
2 changes: 0 additions & 2 deletions airavata-python-sdk/airavata_sdk/clients/plan_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_plan_service_stub(self.channel)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_research_hub_service_stub(self.channel)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_research_project_service_stub(self.channel)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_research_resource_service_stub(self.channel)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_research_session_service_stub(self.channel)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_sharing_service_stub(self.channel)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_gateway_service_stub(self.channel)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ def __init__(self, access_token: Optional[str] = None, claims: Optional[dict] =
self._metadata: list[tuple[str, str]] = []
if access_token:
self._metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
self._metadata.append(("x-claims", json.dumps(claims)))

self._stub = create_user_profile_service_stub(self.channel)

Expand Down
4 changes: 0 additions & 4 deletions airavata-python-sdk/airavata_sdk/transport/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ def __init__(self, access_token: str, claims: Optional[dict] = None):

def __call__(self, context, callback):
metadata = [("authorization", f"Bearer {self.access_token}")]
if self.claims:
metadata.append(("x-claims", json.dumps(self.claims)))
callback(metadata, None)


Expand All @@ -61,8 +59,6 @@ def build_metadata(access_token: Optional[str] = None, claims: Optional[dict] =
metadata = []
if access_token:
metadata.append(("authorization", f"Bearer {access_token}"))
if claims:
metadata.append(("x-claims", json.dumps(claims)))
return metadata


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,18 @@
*/
package org.apache.airavata.server.grpc.config;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
import org.apache.airavata.config.Constants;
import org.apache.airavata.model.security.proto.AuthzToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Shared parsing of the Bearer access token and the {@code x-claims} header used by both the gRPC
* ({@link GrpcAuthInterceptor}) and HTTP ({@link HttpAuthDecorator}) authentication paths. Transport-specific
* concerns (rejecting missing tokens, per-transport header fallbacks, UserContext lifecycle) stay in the callers.
* Shared Bearer-token handling for the gRPC ({@link GrpcAuthInterceptor}) and HTTP ({@link HttpAuthDecorator})
* authentication paths. Identity and roles come solely from the signature-verified token ({@link VerifiedToken});
* client-asserted headers (e.g. {@code x-claims}) are never consulted.
*/
public final class AuthTokenExtractor {

private static final Logger log = LoggerFactory.getLogger(AuthTokenExtractor.class);
private static final ObjectMapper objectMapper = new ObjectMapper();

private AuthTokenExtractor() {}

/** Returns the bearer access token, or {@code null} if the header is absent or not a Bearer token. */
Expand All @@ -48,28 +41,22 @@ public static String stripBearer(String authHeader) {
return null;
}

/** Parses the {@code x-claims} JSON header into a mutable claims map; empty on absence or parse failure. */
public static Map<String, String> parseClaims(String claimsHeader) {
if (claimsHeader != null && !claimsHeader.isBlank()) {
try {
return objectMapper.readValue(claimsHeader, new TypeReference<Map<String, String>>() {});
} catch (Exception e) {
log.warn("Failed to parse x-claims: {}", e.getMessage());
}
}
return new HashMap<>();
}

/**
* Builds an AuthzToken from the access token and (possibly augmented) claims map. The caller's realm roles
* are derived from the verified access token (not the client-asserted {@code x-claims}) and written into the
* claims map under {@link Constants#REALM_ROLES} as a CSV, overwriting any client-supplied value.
* Builds an {@link AuthzToken} whose claims map (user, gateway, realm roles) is populated solely from the
* verified token. Downstream {@code UserContext.userId()/gatewayId()/roles()} read these verified values.
*/
public static AuthzToken buildAuthzToken(String accessToken, Map<String, String> claimsMap) {
claimsMap.put(Constants.REALM_ROLES, String.join(",", JwtVerifier.verifyAndExtractRoles(accessToken)));
public static AuthzToken buildAuthzToken(String accessToken, VerifiedToken verified) {
Map<String, String> claims = new HashMap<>();
if (verified.userName() != null) {
claims.put(Constants.USER_NAME, verified.userName());
}
if (verified.gatewayId() != null) {
claims.put(Constants.GATEWAY_ID, verified.gatewayId());
}
claims.put(Constants.REALM_ROLES, String.join(",", verified.roles()));
return AuthzToken.newBuilder()
.setAccessToken(accessToken)
.putAllClaimsMap(claimsMap)
.putAllClaimsMap(claims)
.build();
}
}
Loading
Loading