Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-04-10 08:39:02

0001 from pandaserver.config import panda_config
0002 from pandaserver.srvcore.CoreUtils import CacheDict
0003 
0004 if panda_config.token_authType is None:
0005     pass
0006 elif panda_config.token_authType == "scitokens":
0007     import scitokens
0008 else:
0009     from pandaserver.srvcore.oidc_utils import TokenDecoder
0010 
0011     token_decoder = TokenDecoder()
0012 
0013 import traceback
0014 
0015 import jwt
0016 
0017 cache_dict = CacheDict()
0018 
0019 
0020 # decode token
0021 def decode_token(serialized_token, env, tmp_log):
0022     authenticated = False
0023     message_str = None
0024     subprocess_env = {}
0025     try:
0026         vo = None
0027         role = None
0028         if panda_config.token_authType == "oidc":
0029             if "HTTP_ORIGIN" in env:
0030                 # two formats, vo:role and vo.role
0031                 vo = env["HTTP_ORIGIN"]
0032                 # only vo.role for auth filename which is a key of auth_vo_dict
0033             else:
0034                 vo = None
0035             # only vo.role for auth filename which is a key of auth_vo_dict
0036             vo_role = vo.replace(":", ".")
0037             token = token_decoder.deserialize_token(serialized_token, panda_config.auth_config, vo, tmp_log, panda_config.legacy_token_issuers)
0038             # extract role
0039             if "vo" in token:
0040                 vo_raw = token["vo"]  # Original input value of vo
0041                 tmp_log.debug(f"Raw VO from token: {vo_raw}")
0042 
0043                 if vo_raw.startswith("vo."):
0044                     # Handle vo names starting with "vo."
0045                     if ":" in vo_raw:
0046                         vo, role = vo_raw.split(":", 1)  # Split by ':' into vo and role
0047                     else:
0048                         vo, role = vo_raw, None  # No role present
0049                 else:
0050                     # Handle cases without "vo." prefix
0051                     if ":" in vo_raw:
0052                         vo, role = vo_raw.split(":", 1)
0053                     elif "." in vo_raw:
0054                         parts = vo_raw.rsplit(".", 1)  # Split only on the last '.'
0055                         vo, role = parts[0], parts[1]
0056                     else:
0057                         vo, role = vo_raw, None  # Single part, no role
0058 
0059             tmp_log.debug(f"Parsed VO: {vo}, role: {role}")
0060 
0061             # check vo
0062             if vo not in panda_config.auth_policies:
0063                 tmp_log.error(f"VO '{vo}' not found in auth_policies: {list(panda_config.auth_policies.keys())}")
0064                 message_str = f"Unknown vo : {vo}"
0065             else:
0066                 # groups claim name based on JWT profile
0067                 groups_claim_name = "groups"
0068                 if vo_role in panda_config.auth_vo_dict:
0069                     jwt_profile = panda_config.auth_vo_dict[vo_role].get("jwt_profile")
0070                     if jwt_profile == "wlcg":
0071                         groups_claim_name = "wlcg.groups"
0072                 # robot
0073                 if vo_role in panda_config.auth_vo_dict and "robot_ids" in panda_config.auth_vo_dict[vo_role]:
0074                     robot_ids = panda_config.auth_vo_dict[vo_role].get("robot_ids")
0075                     if isinstance(robot_ids, str):
0076                         robot_ids = robot_ids.split(",")
0077                     if not robot_ids:
0078                         robot_ids = []
0079                     robot_ids = [i for i in robot_ids if i]
0080                     if token["sub"] in robot_ids:
0081                         if groups_claim_name not in token:
0082                             if role:
0083                                 token[groups_claim_name] = [f"{vo}/{role}"]
0084                             else:
0085                                 token[groups_claim_name] = [f"{vo}"]
0086                         if "name" not in token:
0087                             token["name"] = f"robot {role}"
0088                 # check role
0089                 if role:
0090                     if f"{vo}/{role}" not in token[groups_claim_name] and f"/{vo}/{role}" not in token[groups_claim_name]:
0091                         message_str = f"Not a member of the {vo}/{role} group"
0092                     else:
0093                         subprocess_env["PANDA_OIDC_VO"] = vo
0094                         subprocess_env["PANDA_OIDC_GROUP"] = role
0095                         subprocess_env["PANDA_OIDC_ROLE"] = role
0096                         authenticated = True
0097                 else:
0098                     for member_string, member_info in panda_config.auth_policies[vo]:
0099                         if member_string in token[groups_claim_name] or f"/{member_string}" in token[groups_claim_name]:
0100                             subprocess_env["PANDA_OIDC_VO"] = vo
0101                             subprocess_env["PANDA_OIDC_GROUP"] = member_info["group"]
0102                             subprocess_env["PANDA_OIDC_ROLE"] = member_info["role"]
0103                             authenticated = True
0104                             break
0105                     if not authenticated:
0106                         message_str = f"Not a member of the {vo} group"
0107         else:
0108             token = scitokens.SciToken.deserialize(serialized_token, audience=panda_config.token_audience)
0109 
0110         # check issuer
0111         if "iss" not in token:
0112             message_str = "Issuer is undefined in the token"
0113         else:
0114             if panda_config.token_authType == "scitokens":
0115                 items = token.claims()
0116             else:
0117                 items = token.items()
0118             for c, v in items:
0119                 subprocess_env[f"PANDA_OIDC_CLAIM_{str(c)}"] = str(v)
0120             # use sub and scope as DN and FQAN
0121             if "SSL_CLIENT_S_DN" not in env:
0122                 if "name" in token:
0123                     subprocess_env["SSL_CLIENT_S_DN"] = " ".join([t[:1].upper() + t[1:].lower() for t in str(token["name"]).split()])
0124                     if "preferred_username" in token:
0125                         subprocess_env["SSL_CLIENT_S_DN"] += f"/CN=nickname:{token['preferred_username']}"
0126                 else:
0127                     subprocess_env["SSL_CLIENT_S_DN"] = str(token["sub"])
0128                 i = 0
0129                 for scope in token.get("scope", "").split():
0130                     if scope.startswith("role:"):
0131                         subprocess_env[f"GRST_CRED_AUTH_TOKEN_{i}"] = "VOMS " + str(scope.split(":")[-1])
0132                         i += 1
0133                 if role:
0134                     subprocess_env[f"GRST_CRED_AUTH_TOKEN_{i}"] = f"VOMS /{vo}/Role={role}"
0135                     i += 1
0136             else:
0137                 # protection against cached decisions that miss x509-related variables due to token+x509 access
0138                 subprocess_env["SSL_CLIENT_S_DN"] = env["SSL_CLIENT_S_DN"]
0139                 for key in env:
0140                     if key.startswith("GRST_CRED_") or key.startswith("GRST_CONN_"):
0141                         subprocess_env[key] = env[key]
0142 
0143     except jwt.exceptions.InvalidTokenError as e:
0144         message_str = f"Invalid token. {str(e)}"
0145     except Exception as e:
0146         message_str = f"Token decode failure. {str(e)} {traceback.format_exc()}"
0147     return {"authenticated": authenticated, "message": message_str, "subprocess_env": subprocess_env}
0148 
0149 
0150 # PanDA request object
0151 class PandaRequest:
0152     def __init__(self, env, tmp_log):
0153         # environment
0154         self.subprocess_env = env
0155         # header
0156         self.headers_in = {}
0157         # authentication
0158         self.authenticated = True
0159         # message
0160         self.message = None
0161 
0162         # content-length
0163         if "CONTENT_LENGTH" in self.subprocess_env:
0164             self.headers_in["content-length"] = self.subprocess_env["CONTENT_LENGTH"]
0165 
0166         # tokens
0167         try:
0168             if panda_config.token_authType in ["scitokens", "oidc"] and "HTTP_AUTHORIZATION" in env:
0169                 serialized_token = env["HTTP_AUTHORIZATION"].split()[1]
0170                 decision_key = f"""{serialized_token} : {env.get("HTTP_ORIGIN", None)} : {env.get("SSL_CLIENT_S_DN", None)}"""
0171                 cached_decision = cache_dict.get(decision_key, tmp_log, decode_token, serialized_token, env, tmp_log)
0172                 self.authenticated = cached_decision["authenticated"]
0173                 self.message = cached_decision["message"]
0174                 if self.message:
0175                     tmp_log.error(f"""{self.message} - Origin: {env.get("HTTP_ORIGIN", None)}, Token: {env["HTTP_AUTHORIZATION"]}""")
0176                 self.subprocess_env.update(cached_decision["subprocess_env"])
0177         except Exception as e:
0178             self.message = f"Failed to instantiate reqeust object. {str(e)}"
0179             tmp_log.error(
0180                 f"""{self.message} - Origin: {env.get("HTTP_ORIGIN", None)}, Token: {env.get("HTTP_AUTHORIZATION", None)}\n{traceback.format_exc()}"""
0181             )
0182 
0183     # get remote host
0184     def get_remote_host(self):
0185         if "REMOTE_HOST" in self.subprocess_env:
0186             return self.subprocess_env["REMOTE_HOST"]
0187         return ""
0188 
0189     # accept json
0190     def acceptJson(self):
0191         try:
0192             if "HTTP_ACCEPT" in self.subprocess_env:
0193                 return "application/json" in self.subprocess_env["HTTP_ACCEPT"]
0194         except Exception:
0195             pass
0196         return False