File indexing completed on 2026-04-10 08:39:00
0001 import json
0002 import os
0003 import socket
0004 from threading import Lock
0005 from typing import List
0006
0007 from pandacommon.pandalogger.LogWrapper import LogWrapper
0008 from pandacommon.pandalogger.PandaLogger import PandaLogger
0009 from pandacommon.pandautils.PandaUtils import naive_utcnow
0010
0011 from pandaserver.api.v1.common import generate_response, get_dn, request_validation
0012 from pandaserver.config import panda_config
0013 from pandaserver.proxycache import panda_proxy_cache, token_cache
0014 from pandaserver.srvcore import CoreUtils
0015 from pandaserver.srvcore.CoreUtils import clean_user_id, get_bare_dn
0016 from pandaserver.srvcore.panda_request import PandaRequest
0017 from pandaserver.taskbuffer.TaskBuffer import TaskBuffer
0018
0019 _logger = PandaLogger().getLogger("api_cred_management")
0020
0021
0022 global_task_buffer = None
0023 global_dispatch_parameter_cache = None
0024
0025
0026 global_proxy_cache = panda_proxy_cache.MyProxyInterface()
0027 global_token_cache = token_cache.TokenCache()
0028
0029 global_lock = Lock()
0030
0031
0032 def init_task_buffer(task_buffer: TaskBuffer) -> None:
0033 """
0034 Initialize the task buffer. This method needs to be called before any other method in this module.
0035 """
0036
0037 with global_lock:
0038 global global_task_buffer
0039 global_task_buffer = task_buffer
0040
0041
0042 global global_dispatch_parameter_cache
0043 global_dispatch_parameter_cache = CoreUtils.CachedObject("dispatcher_params", 60 * 10, _get_dispatch_parameters, _logger)
0044
0045 global global_token_cache_config
0046 global_token_cache_config = _read_token_cache_configuration()
0047
0048
0049 def _read_token_cache_configuration():
0050
0051 try:
0052 with open(panda_config.token_cache_config) as f:
0053 return json.load(f)
0054 except Exception:
0055 return {}
0056
0057
0058 def _get_dispatch_parameters():
0059 """
0060 Wrapper function around taskBuffer.get_special_dispatch_params to convert list to set since task buffer cannot return set
0061 """
0062 parameters = global_task_buffer.get_special_dispatch_params()
0063 for client_name, client_data in parameters["tokenKeys"].items():
0064 client_data["fullList"] = set(client_data["fullList"])
0065
0066 return True, parameters
0067
0068
0069 def _validate_user_permissions(compact_name, tokenized=False) -> dict:
0070 allowed_names = global_dispatch_parameter_cache.get("allowProxy", [])
0071
0072
0073 if compact_name in allowed_names:
0074 return True, ""
0075
0076
0077 secret = "proxy" if not tokenized else "access token"
0078 tmp_msg = f"failed: '{compact_name}' not in authorized users with 'p' in {panda_config.schemaMETA}.USERS.GRIDPREF to get {secret}"
0079
0080 return False, tmp_msg
0081
0082
0083 @request_validation(_logger, secure=True, request_method="POST")
0084 def set_user_secrets(req: PandaRequest, key: str = None, value: str = None) -> dict:
0085 """
0086 Set user secrets
0087
0088 Set a key-value pair to store in PanDA. Requires a secure connection.
0089
0090 API details:
0091 HTTP Method: POST
0092 Path: /v1/creds/set_user_secrets
0093
0094 Args:
0095 req(PandaRequest): internally generated request object
0096 key(str): Key to reference the secret
0097 value(str): Value of the secret
0098
0099 Returns:
0100 dict: The system response. True for success, False for failure, and an error message.
0101 """
0102
0103 tmp_logger = LogWrapper(_logger, f"set_user_secret-{naive_utcnow().isoformat('/')}")
0104
0105
0106 dn = get_dn(req)
0107 owner = clean_user_id(dn)
0108 tmp_logger.debug(f"Setting secret for client={owner}")
0109 success, message = global_task_buffer.set_user_secret(owner, key, value)
0110 return generate_response(success, message)
0111
0112
0113 @request_validation(_logger, secure=True, request_method="GET")
0114 def get_user_secrets(req: PandaRequest, keys: List[str] = None) -> dict:
0115 """
0116 Get user secrets
0117
0118 Get the secrets for a user identified by a list of keys. Requires a secure connection.
0119
0120 API details:
0121 HTTP Method: POST
0122 Path: /v1/creds/get_user_secrets
0123
0124 Args:
0125 req(PandaRequest): internally generated request object
0126 keys(list of str, optional): List of keys to reference the secrets to retrieve
0127
0128 Returns:
0129 dict: The system response `{"success": success, "message": message, "data": data}`. When successful, the data field contains the user secrets. When unsuccessful, the message field contains the error message.
0130 """
0131 tmp_logger = LogWrapper(_logger, f"get_user_secrets-{naive_utcnow().isoformat('/')}")
0132
0133
0134 dn = get_dn(req)
0135 owner = clean_user_id(dn)
0136 tmp_logger.debug(f"Start for client={owner}")
0137
0138
0139 if keys is None:
0140 keys_str = ""
0141 else:
0142 keys_str = ",".join(keys)
0143
0144 success, data_or_message = global_task_buffer.get_user_secrets(owner, keys_str)
0145 message, data = "", {}
0146 if success:
0147 data = data_or_message
0148 else:
0149 message = data_or_message
0150 tmp_logger.debug(f"Done for client={owner}")
0151 return generate_response(success, message, data)
0152
0153
0154 @request_validation(_logger, secure=True, request_method="GET")
0155 def get_key_pair(req: PandaRequest, public_key_name: str, private_key_name: str) -> dict:
0156 """
0157 Get key pair
0158
0159 This function retrieves the distinguished name (DN) from the request and uses it to get a key pair. Requires a secure connection.
0160
0161 API details:
0162 HTTP Method: GET
0163 Path: /v1/creds/get_key_pair
0164
0165 Args:
0166 req(PandaRequest): internally generated request object
0167 public_key_name(str): The name of the public key.
0168 private_key_name(str): The name of the private key.
0169
0170 Returns:
0171 dict: The system response `{"success": success, "message": message, "data": data}`. When successful, the data field contains the user secrets. When unsuccessful, the message field contains the error message.
0172 """
0173 tmp_logger = LogWrapper(_logger, f"get_key_pair {public_key_name}/{private_key_name}")
0174 tmp_logger.debug("Start")
0175
0176 real_dn = get_dn(req)
0177
0178
0179 compact_dn = clean_user_id(real_dn)
0180
0181
0182 global_dispatch_parameter_cache.update()
0183 allowed_keys = global_dispatch_parameter_cache.get("allowKeyPair", [])
0184 if compact_dn not in allowed_keys:
0185
0186 tmp_msg = f"Failed since '{compact_dn}' not authorized with 'k' in {panda_config.schemaMETA}.USERS.GRIDPREF"
0187 tmp_logger.debug(tmp_msg)
0188 return generate_response(False, tmp_msg)
0189
0190
0191 key_pair_dict = global_dispatch_parameter_cache.get("keyPair", {})
0192
0193 if public_key_name not in key_pair_dict:
0194 tmp_msg = f"Failed for '{compact_dn}' since {public_key_name} is missing on {socket.getfqdn()}"
0195 tmp_logger.debug(tmp_msg)
0196 return generate_response(False, tmp_msg)
0197
0198 if private_key_name not in key_pair_dict:
0199 tmp_msg = f"Failed for '{compact_dn}' since {private_key_name} is missing on {socket.getfqdn()}"
0200 tmp_logger.debug(tmp_msg)
0201 return generate_response(False, tmp_msg)
0202
0203
0204 data = {"public_key": key_pair_dict[public_key_name], "private_key": key_pair_dict[private_key_name]}
0205 tmp_logger.debug(f"Done with key-pair for '{compact_dn}'")
0206
0207 return generate_response(True, data=data)
0208
0209
0210 @request_validation(_logger, secure=True, request_method="GET")
0211 def get_proxy(req: PandaRequest, role: str = None, dn: str = None) -> dict:
0212 """
0213 Get proxy
0214
0215 Get the x509 proxy certificate for a user with a role. Requires a secure connection.
0216
0217 API details:
0218 HTTP Method: GET
0219 Path: /v1/creds/get_proxy
0220
0221 Args:
0222 req(PandaRequest): internally generated request object
0223 role(str, optional): The role of the user. Defaults to None.
0224 dn(str, optional): The distinguished name of the user. Defaults to None.
0225
0226 Returns:
0227 dict: The system response `{"success": success, "message": message, "data": data}`. When successful, the data field contains the x509 proxy. When unsuccessful, the message field contains the error message.
0228 """
0229 tmp_logger = LogWrapper(_logger, f"get_proxy PID={os.getpid()}")
0230
0231
0232 real_dn = get_dn(req)
0233 target_dn = dn if dn else real_dn
0234
0235
0236 if role == "":
0237 role = None
0238
0239 tmp_logger.debug(f"Start for DN='{real_dn}' role={role}")
0240
0241
0242 global_dispatch_parameter_cache.update()
0243 real_dn_compact = clean_user_id(real_dn)
0244 allowed, tmp_msg = _validate_user_permissions(real_dn_compact, tokenized=False)
0245 if not allowed:
0246 tmp_logger.debug(tmp_msg)
0247 return generate_response(False, tmp_msg)
0248
0249
0250 target_dn_bare = get_bare_dn(target_dn, keep_digits=False)
0251 output = global_proxy_cache.retrieve(target_dn_bare, role=role)
0252
0253
0254 if output is None:
0255 tmp_msg = f"'proxy' not found for {target_dn_bare}"
0256 tmp_logger.debug(tmp_msg)
0257 return generate_response(False, tmp_msg)
0258
0259 data = {"user_proxy": output}
0260 tmp_logger.debug(f"Done")
0261 return generate_response(True, data=data)
0262
0263
0264 @request_validation(_logger, secure=True, request_method="GET")
0265 def get_access_token(req: PandaRequest, client_name: str, token_key: str = None) -> dict:
0266 """
0267 Get access token
0268
0269 Get the OAuth access token for the specified client. Requires a secure connection.
0270
0271 API details:
0272 HTTP Method: GET
0273 Path: /v1/creds/get_access_token
0274
0275 Args:
0276 req(PandaRequest): internally generated request object
0277 client_name(str): client_name for the token as defined in token_cache_config
0278 token_key(str, optional): key to get the token from the token cache. Defaults to None.
0279
0280 Returns:
0281 dict: The system response `{"success": success, "message": message, "data": data}`. When successful, the data field contains the access token. When unsuccessful, the message field contains the error message.
0282 """
0283
0284 tmp_logger = LogWrapper(_logger, f"get_proxy PID={os.getpid()}")
0285
0286
0287 real_dn = get_dn(req)
0288 target_dn = client_name if client_name else real_dn
0289
0290 tmp_logger.debug(f"Start for DN='{real_dn}' target_dn='{target_dn}' token_key={token_key}")
0291
0292
0293 global_dispatch_parameter_cache.update()
0294 real_dn_compact = clean_user_id(real_dn)
0295 allowed, tmp_msg = _validate_user_permissions(real_dn_compact, tokenized=True)
0296 if not allowed:
0297 tmp_logger.debug(tmp_msg)
0298 return generate_response(False, tmp_msg)
0299
0300
0301 if target_dn in global_token_cache_config and global_token_cache_config[target_dn].get("use_token_key") is True:
0302 if (
0303 target_dn not in global_dispatch_parameter_cache["tokenKeys"]
0304 or token_key not in global_dispatch_parameter_cache["tokenKeys"][target_dn]["fullList"]
0305 ):
0306 tmp_msg = f"failed since token key is invalid for {target_dn}"
0307 tmp_logger.debug(tmp_msg)
0308 return generate_response(False, tmp_msg)
0309
0310
0311 target_dn_bare = get_bare_dn(target_dn, keep_digits=False)
0312
0313
0314 output = global_token_cache.get_access_token(target_dn_bare)
0315
0316
0317 if output is None:
0318 tmp_msg = f"'token' not found for {target_dn_bare}"
0319 tmp_logger.debug(tmp_msg)
0320 return generate_response(False, tmp_msg)
0321
0322 data = {"user_proxy": output}
0323 tmp_logger.debug("Done")
0324 return generate_response(True, data=data)
0325
0326
0327 @request_validation(_logger, secure=True, request_method="GET")
0328 def get_token_key(req: PandaRequest, client_name: str) -> dict:
0329 """
0330 Get token key
0331
0332 This function retrieves the distinguished name (DN) from the request and uses it to get a token key for the specified client. Requires a secure connection.
0333
0334 API details:
0335 HTTP Method: GET
0336 Path: /v1/creds/get_token_key
0337
0338 Args:
0339 req(PandaRequest): internally generated request object
0340 client_name (str): The name of the client requesting the token key
0341
0342 Returns:
0343 dict: The system response `{"success": success, "message": message, "data": data}`. When successful, the data field contains the token key. When unsuccessful, the message field contains the error message.
0344 """
0345 tmp_logger = LogWrapper(_logger, f"get_token_key client={client_name} PID={os.getpid()}")
0346 tmp_logger.debug("Start")
0347
0348
0349 real_dn = get_dn(req)
0350 compact_name = clean_user_id(real_dn)
0351
0352 global_dispatch_parameter_cache.update()
0353
0354
0355 allowed_users = global_dispatch_parameter_cache.get("allowTokenKey", [])
0356 if compact_name not in allowed_users:
0357
0358 tmp_msg = f"Denied since '{compact_name}' not authorized with 't' in {panda_config.schemaMETA}.USERS.GRIDPREF"
0359 tmp_logger.debug(tmp_msg)
0360 return generate_response(False, tmp_msg)
0361
0362
0363 if client_name not in global_dispatch_parameter_cache["tokenKeys"]:
0364 tmp_msg = f"Token key is missing for '{client_name}'"
0365 tmp_logger.debug(tmp_msg)
0366 return generate_response(False, tmp_msg)
0367
0368
0369 data = {"tokenKey": global_dispatch_parameter_cache["tokenKeys"][client_name]["latest"]}
0370 tmp_logger.debug(f"Done getting token key for '{compact_name}'")
0371
0372 return generate_response(True, data=data)