Back to home page

EIC code displayed by LXR

 
 

    


Warning, /eic-spack/spack_repo/eic/packages/py_rucio_clients/token-upload-client-40.2.0.patch is written in an unsupported language. File is not indexed.

0001 diff --git a/lib/rucio/client/credentialclient.py b/lib/rucio/client/credentialclient.py
0002 index 8e8bdd6f1..52e6257b8 100644
0003 --- a/lib/rucio/client/credentialclient.py
0004 +++ b/lib/rucio/client/credentialclient.py
0005 @@ -12,6 +12,9 @@
0006  # See the License for the specific language governing permissions and
0007  # limitations under the License.
0008  
0009 +import json
0010 +from typing import Any, Literal, Optional
0011 +
0012  from requests.status_codes import codes
0013  
0014  from rucio.client.baseclient import BaseClient, choice
0015 @@ -31,6 +34,7 @@ class CredentialClient(BaseClient):
0016      """
0017  
0018      CREDENTIAL_BASEURL = 'credentials'
0019 +    TOKEN_BASEURL = 'tokens'
0020  
0021      def get_signed_url(
0022              self,
0023 @@ -108,3 +112,99 @@ class CredentialClient(BaseClient):
0024  
0025          exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
0026          raise exc_cls(exc_msg)
0027 +
0028 +    def _post_token_request(self, data: dict[str, Any]) -> str:
0029 +        """Send a sesson token request to the server and return the minted storage token."""
0030 +        path = '/'.join([self.TOKEN_BASEURL, 'request'])
0031 +        rurl = build_url(choice(self.list_hosts), path=path)
0032 +        r = self._send_request(rurl, method=HTTPMethod.POST, data=json.dumps(data))
0033 +        if r.status_code == codes.ok:
0034 +            return json.loads(r.text)['token']
0035 +        exc_cls, exc_msg = self._get_exception(headers=r.headers,
0036 +                                            status_code=r.status_code,
0037 +                                            data=r.content)
0038 +        raise exc_cls(exc_msg)
0039 +
0040 +
0041 +    def get_upload_token(
0042 +        self,
0043 +        rse: str,
0044 +        scope: str,
0045 +        name: str,
0046 +        domain: Literal["wan", "lan"] = "wan",
0047 +        register_after_upload: bool = False,
0048 +        no_register: bool = False,
0049 +        scheme: Optional[str] = None,
0050 +    ) -> str:
0051 +        """
0052 +        Request a file-scoped OIDC token for uploading a single DID to an RSE.
0053 +
0054 +        The token authorizes writing exactly the ``scope:name`` DID on ``rse``.
0055 +        Call it only after the DID is registered — the server refuses tokens for
0056 +        unregistered DIDs.
0057 +
0058 +        Parameters
0059 +        ----------
0060 +        rse
0061 +            The name of the RSE to upload to.
0062 +        scope
0063 +            DID scope of the file to upload.
0064 +        name
0065 +            DID name of the file to upload.
0066 +        domain
0067 +            Network domain the token applies to: ``"wan"`` or ``"lan"``.
0068 +            Defaults to ``"wan"``.
0069 +        register_after_upload
0070 +            Whether the file is registered after the upload completes.
0071 +            Defaults to ``False``.
0072 +        no_register
0073 +            Whether to skip registration entirely. Defaults to ``False``.
0074 +        scheme
0075 +            The scheme the upload is forced to (``force_scheme``), or None to
0076 +            cover every write protocol of the RSE.
0077 +
0078 +        Returns
0079 +        -------
0080 +        str
0081 +            The OIDC access token, valid until it expires.
0082 +
0083 +        Raises
0084 +        ------
0085 +        RucioException
0086 +            If the server returns a status code other than ``200 OK``,
0087 +            including when the DID is not yet registered.
0088 +        """
0089 +        return self._post_token_request({
0090 +            'rse': rse,
0091 +            'operation': 'upload',
0092 +            'scope': scope,
0093 +            'name': name,
0094 +            'domain': domain,
0095 +            'register_after_upload': register_after_upload,
0096 +            'no_register': no_register,
0097 +            'scheme': scheme,
0098 +        })
0099 +
0100 +    def get_download_token(self, rse: str) -> str:
0101 +        """
0102 +        Request an RSE-scoped OIDC token for downloading from an RSE.
0103 +
0104 +        The token authorises reading from ``rse`` and is not tied to a specific
0105 +        DID, so a single token can serve many files on that RSE.
0106 +
0107 +        Parameters
0108 +        ----------
0109 +        rse
0110 +            The name of the RSE to download from.
0111 +
0112 +        Returns
0113 +        -------
0114 +        str
0115 +            The OIDC access token, valid until it expires.
0116 +
0117 +        Raises
0118 +        ------
0119 +        RucioException
0120 +            If the server returns a status code other than ``200 OK``.
0121 +        """
0122 +        return self._post_token_request({'rse': rse, 'operation': 'download'})
0123 diff --git a/lib/rucio/client/uploadclient.py b/lib/rucio/client/uploadclient.py
0124 index 3ca6a0688..f58768a02 100644
0125 --- a/lib/rucio/client/uploadclient.py
0126 +++ b/lib/rucio/client/uploadclient.py
0127 @@ -50,9 +50,23 @@ from rucio.common.utils import execute, generate_uuid, make_valid_did, retry, se
0128  from rucio.rse import rsemanager as rsemgr
0129  
0130  if TYPE_CHECKING:
0131 -    from collections.abc import Iterable, Mapping
0132 -
0133 -    from rucio.common.types import AttachDict, DatasetDict, DIDStringDict, FileToUploadDict, FileToUploadWithCollectedAndDatasetInfoDict, FileToUploadWithCollectedInfoDict, LFNDict, LoggerFunction, PathTypeAlias, RSESettingsDict, TraceBaseDict, TraceDict
0134 +    from collections.abc import Iterable, Mapping, Sequence
0135 +
0136 +    from rucio.common.types import (
0137 +        AttachDict,
0138 +        DatasetDict,
0139 +        DIDStringDict,
0140 +        FileToUploadDict,
0141 +        FileToUploadWithCollectedAndDatasetInfoDict,
0142 +        FileToUploadWithCollectedInfoDict,
0143 +        LFNDict,
0144 +        LoggerFunction,
0145 +        PathTypeAlias,
0146 +        RSEProtocolDict,
0147 +        RSESettingsDict,
0148 +        TraceBaseDict,
0149 +        TraceDict,
0150 +    )
0151      from rucio.rse.protocols.protocol import RSEProtocol
0152  
0153  
0154 @@ -91,10 +105,6 @@ class UploadClient:
0155  
0156          self.client: Final[Client] = _client if _client else Client()
0157          self.client_location = detect_client_location()
0158 -        # if token should be used, use only JWT tokens
0159 -        self.auth_token: Optional[str] = (
0160 -            self.client.auth_token if len(self.client.auth_token.split(".")) == 3 else None
0161 -        )
0162          self.tracing = tracing
0163          if not self.tracing:
0164              logger(logging.DEBUG, 'Tracing is turned off.')
0165 @@ -438,6 +448,41 @@ class UploadClient:
0166                                      ignore_availability=ignore_availability,
0167                                      activity=activity)
0168  
0169 +            # write protocols in priority order, used by the token existence check
0170 +            # and the upload loop below (computed once). Raises RSEProtocolNotSupported
0171 +            # if none match, before any token is requested.
0172 +            write_protocols = rsemgr.get_protocols_ordered(rse_settings=rse_settings,
0173 +                                                           operation='write',
0174 +                                                           scheme=force_scheme,
0175 +                                                           domain=domain,
0176 +                                                           impl=impl)
0177 +
0178 +            # Storage bearer for this file. Defaults to None, meaning "no bearer"
0179 +            # (the protocol falls back to its own cert/x509 auth) -- never the
0180 +            # Rucio session token. On an RSE flagged OIDC_SUPPORT we exchange the
0181 +            # Rucio auth for a path-scoped storage token minted by the server.
0182 +            # This request must happen after _register_file: the token API
0183 +            # refuses tokens for unregistered DIDs. It is skipped for
0184 +            # register_after_upload (DID not yet registered) and for explicit
0185 +            # pfn uploads.
0186 +            upload_auth_token = None
0187 +            if rse_attributes.get(RseAttr.OIDC_SUPPORT) \
0188 +                    and not register_after_upload and not pfn \
0189 +                    and (not no_register or rse_attributes.get('no_register')):
0190 +                try:
0191 +                    upload_auth_token = self.client.get_upload_token(
0192 +                        rse, file['did_scope'], file['did_name'], domain=domain, scheme=force_scheme,
0193 +                        no_register=bool(no_register))
0194 +                    logger(logging.DEBUG, 'Using scoped upload token for %s:%s at %s'
0195 +                           % (file['did_scope'], file['did_name'], rse))
0196 +                except Exception as error:
0197 +                    # fall back to the RSE's other auth (e.g. x509) rather than failing the file.
0198 +                    logger(logging.WARNING,
0199 +                           'Could not obtain scoped upload token for %s:%s at %s, '
0200 +                           'falling back to non-token auth: %s'
0201 +                           % (file['did_scope'], file['did_name'], rse, str(error)))
0202 +                    upload_auth_token = None
0203 +
0204              # if register_after_upload, the file should be overwritten if it is not registered,
0205              # otherwise if the file already exists on RSE we're done
0206              if register_after_upload:
0207 @@ -446,7 +491,7 @@ class UploadClient:
0208                                   domain=domain,
0209                                   scheme=force_scheme,
0210                                   impl=impl,
0211 -                                 auth_token=self.auth_token,
0212 +                                 auth_token=upload_auth_token,
0213                                   vo=self.client.vo,
0214                                   logger=logger):
0215                      try:
0216 @@ -463,7 +508,7 @@ class UploadClient:
0217                                   domain=domain,
0218                                   scheme=force_scheme,
0219                                   impl=impl,
0220 -                                 auth_token=self.auth_token,
0221 +                                 auth_token=upload_auth_token,
0222                                   vo=self.client.vo,
0223                                   logger=logger):
0224                      logger(logging.INFO,
0225 @@ -475,32 +520,39 @@ class UploadClient:
0226                                     domain=domain,
0227                                     scheme=force_scheme,
0228                                     impl=impl,
0229 -                                   auth_token=self.auth_token,
0230 +                                   auth_token=upload_auth_token,
0231                                     vo=self.client.vo,
0232                                     logger=logger):
0233                      logger(logging.INFO, 'File already exists on RSE with different pfn. Skipping upload.')
0234                      trace['stateReason'] = 'File already exists'
0235                      continue
0236              else:
0237 -                if rsemgr.exists(rse_settings,
0238 -                                 pfn if pfn else file_did,  # type: ignore (pfn is str)
0239 -                                 domain=domain,
0240 -                                 scheme=force_scheme,
0241 -                                 impl=impl,
0242 -                                 auth_token=self.auth_token,
0243 -                                 vo=self.client.vo,
0244 -                                 logger=logger):
0245 +                if upload_auth_token:
0246 +                    # With a scoped upload token, check existence on the endpoint the token
0247 +                    # was minted for (the write endpoint). rsemgr.exists() uses the top *read*
0248 +                    # protocol, which may be a different (e.g. open-access, read-only) host
0249 +                    # that the token's audience does not cover.
0250 +                    already_exists = self._exists_on_write_endpoint(rse_settings,
0251 +                                                                    write_protocols,
0252 +                                                                    pfn if pfn else file_did,
0253 +                                                                    domain=domain,
0254 +                                                                    auth_token=upload_auth_token)
0255 +                else:
0256 +                    already_exists = rsemgr.exists(rse_settings,
0257 +                                                   pfn if pfn else file_did,  # type: ignore (pfn is str)
0258 +                                                   domain=domain,
0259 +                                                   scheme=force_scheme,
0260 +                                                   impl=impl,
0261 +                                                   auth_token=upload_auth_token,
0262 +                                                   vo=self.client.vo,
0263 +                                                   logger=logger)
0264 +                if already_exists:
0265                      logger(logging.INFO, 'File already exists on RSE. Skipping upload')
0266                      trace['stateReason'] = 'File already exists'
0267                      continue
0268  
0269              # protocol handling and upload
0270 -            protocols = rsemgr.get_protocols_ordered(rse_settings=rse_settings,
0271 -                                                     operation='write',
0272 -                                                     scheme=force_scheme,
0273 -                                                     domain=domain,
0274 -                                                     impl=impl)
0275 -            protocols.reverse()
0276 +            protocols = list(reversed(write_protocols))
0277              success = False
0278              state_reason = ''
0279              logger(logging.DEBUG, str(protocols))
0280 @@ -536,7 +588,8 @@ class UploadClient:
0281                                              force_pfn=pfn,
0282                                              transfer_timeout=file.get('transfer_timeout'),
0283                                              delete_existing=delete_existing,
0284 -                                            sign_service=sign_service)
0285 +                                            sign_service=sign_service,
0286 +                                            auth_token=upload_auth_token)
0287                      logger(logging.DEBUG, 'Upload done.')
0288                      success = True
0289                      file['upload_result'] = {0: True, 1: None, 'success': True, 'pfn': pfn}  # TODO: needs to be removed
0290 @@ -996,7 +1049,8 @@ class UploadClient:
0291              force_scheme: Optional[str] = None,
0292              transfer_timeout: Optional[int] = None,
0293              delete_existing: bool = False,
0294 -            sign_service: Optional[str] = None
0295 +            sign_service: Optional[str] = None,
0296 +            auth_token: Optional[str] = None
0297      ) -> Optional[str]:
0298          """
0299          Perform the actual file transfer to an RSE using the appropriate protocol.
0300 @@ -1034,6 +1088,10 @@ class UploadClient:
0301              If True, removes any unregistered or stale file on the storage that matches this PFN.
0302          sign_service
0303              If set, requests a signed URL from the given service (e.g., gcs, s3, swift).
0304 +        auth_token
0305 +            The storage bearer token to use for this protocol, or None to use the
0306 +            protocol's own (e.g. cert/x509) authentication. This is the per-file,
0307 +            path-scoped storage token requested from the server
0308  
0309          Returns
0310          -------
0311 @@ -1059,7 +1117,8 @@ class UploadClient:
0312                                                 'write',
0313                                                 force_scheme=force_scheme,
0314                                                 domain=domain,
0315 -                                               impl=impl)
0316 +                                               impl=impl,
0317 +                                               auth_token=auth_token)
0318  
0319          base_name = lfn.get('filename', lfn['name'])
0320          name = lfn.get('name', base_name)
0321 @@ -1087,7 +1146,8 @@ class UploadClient:
0322              protocol_read = self._create_protocol(rse_settings,
0323                                                    'read',
0324                                                    domain=domain,
0325 -                                                  impl=impl)
0326 +                                                  impl=impl,
0327 +                                                  auth_token=auth_token)
0328              if pfn is not None:
0329                  signed_read_pfn = self.client.get_signed_url(rse_settings['rse'], sign_service, 'read', pfn)
0330                  pfn = self.client.get_signed_url(rse_settings['rse'], sign_service, 'write', pfn)
0331 @@ -1116,7 +1176,8 @@ class UploadClient:
0332                                                          'delete',
0333                                                          force_scheme=force_scheme,
0334                                                          domain=domain,
0335 -                                                        impl=impl)
0336 +                                                        impl=impl,
0337 +                                                        auth_token=auth_token)
0338                  delete_pfn = '%s.rucio.upload' % list(protocol_delete.lfns2pfns(make_valid_did(lfn)).values())[0]
0339                  if sign_service:
0340                      delete_pfn = self.client.get_signed_url(rse_settings['rse'], sign_service, 'delete', delete_pfn)
0341 @@ -1135,7 +1196,8 @@ class UploadClient:
0342                                                          'delete',
0343                                                          force_scheme=force_scheme,
0344                                                          domain=domain,
0345 -                                                        impl=impl)
0346 +                                                        impl=impl,
0347 +                                                        auth_token=auth_token)
0348                  delete_pfn = '%s' % list(protocol_delete.lfns2pfns(make_valid_did(lfn)).values())[0]
0349                  if sign_service:
0350                      delete_pfn = self.client.get_signed_url(rse_settings['rse'], sign_service, 'delete', delete_pfn)
0351 @@ -1252,13 +1314,67 @@ class UploadClient:
0352                  time.sleep(2 ** attempt)
0353          return protocol.stat(pfn)
0354  
0355 +    def _exists_on_write_endpoint(
0356 +            self,
0357 +            rse_settings: "RSESettingsDict",
0358 +            write_protocols: "Sequence[RSEProtocolDict]",
0359 +            pfn_or_did: Union[str, dict[str, str]],
0360 +            domain: str = 'wan',
0361 +            auth_token: Optional[str] = None
0362 +    ) -> bool:
0363 +        """
0364 +        Check whether a file already exists, using the *write* protocols of the RSE.
0365 +
0366 +        Used for token-authenticated uploads: the scoped upload token is minted for
0367 +        the write endpoint(s), so the pre-upload existence check must go there too,
0368 +        not to the top-priority read protocol (which may be a different, e.g.
0369 +        open-access read-only, host the token's audience does not cover).
0370 +
0371 +        ``write_protocols`` is the same priority-ordered list the upload loop uses.
0372 +        They are tried in that order, mirroring the loop's fallback: if the check
0373 +        fails on one protocol (e.g. root), the next one (e.g. https) is tried. Only
0374 +        if every protocol fails is the last error raised.
0375 +        """
0376 +        last_error: Optional[Exception] = None
0377 +        for protocol_attr in write_protocols:
0378 +            try:
0379 +                protocol = rsemgr.create_protocol(rse_settings, 'write', domain=domain,
0380 +                                                  auth_token=auth_token, protocol_attr=protocol_attr,
0381 +                                                  logger=self.logger)
0382 +                protocol.connect()
0383 +            except Exception as error:
0384 +                self.logger(logging.WARNING, 'Could not connect with %s for the existence check: %s'
0385 +                            % (protocol_attr['scheme'], error))
0386 +                last_error = error
0387 +                continue
0388 +            try:
0389 +                if isinstance(pfn_or_did, str):
0390 +                    pfn = pfn_or_did
0391 +                else:
0392 +                    pfn = list(protocol.lfns2pfns(make_valid_did(cast("LFNDict", pfn_or_did))).values())[0]
0393 +                    if isinstance(pfn, RucioException):
0394 +                        raise pfn
0395 +                self.logger(logging.DEBUG, 'Checking on write endpoint if %s exists' % pfn)
0396 +                return protocol.exists(pfn)
0397 +            except Exception as error:
0398 +                self.logger(logging.WARNING, 'Existence check with %s failed, trying next write protocol: %s'
0399 +                            % (protocol_attr['scheme'], error))
0400 +                last_error = error
0401 +            finally:
0402 +                protocol.close()
0403 +        if last_error is not None:
0404 +            raise last_error
0405 +        raise RSEOperationNotSupported('No write protocol available for the existence check on %s'
0406 +                                       % rse_settings['rse'])
0407 +
0408      def _create_protocol(
0409              self,
0410              rse_settings: "RSESettingsDict",
0411              operation: str,
0412              impl: Optional[str] = None,
0413              force_scheme: Optional[str] = None,
0414 -            domain: str = 'wan'
0415 +            domain: str = 'wan',
0416 +            auth_token: Optional[str] = None
0417      ) -> "RSEProtocol":
0418          """
0419          Creates and returns the protocol object for the requested RSE operation.
0420 @@ -1278,6 +1394,10 @@ class UploadClient:
0421              If provided, forces the protocol to use this scheme.
0422          domain
0423              The network domain to be used, defaulting to 'wan'.
0424 +        auth_token
0425 +            The storage bearer token to use for this protocol, or None to use the
0426 +            protocol's own (e.g. cert/x509) authentication. This is the per-file,
0427 +            path-scoped storage token requested from the server
0428  
0429          Returns
0430          -------
0431 @@ -1296,7 +1416,7 @@ class UploadClient:
0432                  scheme=force_scheme,
0433                  domain=domain,
0434                  impl=impl,
0435 -                auth_token=self.auth_token,
0436 +                auth_token=auth_token,
0437                  logger=self.logger
0438              )
0439              protocol.connect()
0440 @@ -1430,7 +1550,8 @@ class UploadClient:
0441      def preferred_impl(
0442              self,
0443              rse_settings: "RSESettingsDict",
0444 -            domain: str
0445 +            domain: str,
0446 +            auth_token: Optional[str] = None
0447      ) -> Optional[str]:
0448          """
0449          Select a suitable protocol implementation for read, write, and delete operations on
0450 @@ -1450,6 +1571,10 @@ class UploadClient:
0451          domain
0452              The network domain (e.g., 'lan' or 'wan') in which the protocol must support
0453              all operations.
0454 +        auth_token
0455 +            The storage bearer token to use for this protocol, or None to use the
0456 +            protocol's own (e.g. cert/x509) authentication. This is the per-file,
0457 +            path-scoped storage token requested from the server
0458  
0459          Returns
0460          -------
0461 @@ -1497,7 +1622,7 @@ class UploadClient:
0462                  continue
0463              try:
0464                  supported_protocol = rsemgr.create_protocol(rse_settings, 'write', domain=domain, impl=protocol['impl'],
0465 -                                                            auth_token=self.auth_token, logger=self.logger)
0466 +                                                            auth_token=auth_token, logger=self.logger)
0467                  supported_protocol.connect()
0468              except Exception as error:
0469                  self.logger(logging.DEBUG, 'Failed to create protocol "%s", exception: %s' % (protocol['impl'], error))