diff --git a/src/__init__.py b/cs3client/__init__.py similarity index 100% rename from src/__init__.py rename to cs3client/__init__.py diff --git a/src/app.py b/cs3client/app.py similarity index 80% rename from src/app.py rename to cs3client/app.py index d54ce22..9f2787a 100644 --- a/src/app.py +++ b/cs3client/app.py @@ -3,19 +3,19 @@ Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti. Emails: rasmus.oscar.welander@cern.ch, diogo.castro@cern.ch, giuseppe.lopresti@cern.ch -Last updated: 19/08/2024 +Last updated: 28/08/2024 """ import logging -from auth import Auth -from cs3resource import Resource import cs3.app.registry.v1beta1.registry_api_pb2 as cs3arreg import cs3.app.registry.v1beta1.resources_pb2 as cs3arres import cs3.gateway.v1beta1.gateway_api_pb2 as cs3gw import cs3.app.provider.v1beta1.resources_pb2 as cs3apr from cs3.gateway.v1beta1.gateway_api_pb2_grpc import GatewayAPIStub -from statuscodehandler import StatusCodeHandler -from config import Config + +from cs3client.cs3resource import Resource +from cs3client.statuscodehandler import StatusCodeHandler +from cs3client.config import Config class App: @@ -28,7 +28,6 @@ def __init__( config: Config, log: logging.Logger, gateway: GatewayAPIStub, - auth: Auth, status_code_handler: StatusCodeHandler, ) -> None: """ @@ -37,20 +36,21 @@ def __init__( :param config: Config object containing the configuration parameters. :param log: Logger instance for logging. :param gateway: GatewayAPIStub instance for interacting with CS3 Gateway. - :param auth: An instance of the auth class. :param status_code_handler: An instance of the StatusCodeHandler class. """ self._status_code_handler: StatusCodeHandler = status_code_handler self._gateway: GatewayAPIStub = gateway self._log: logging.Logger = log self._config: Config = config - self._auth: Auth = auth - def open_in_app(self, resource: Resource, view_mode: str = None, app: str = None) -> cs3apr.OpenInAppURL: + def open_in_app( + self, auth_token: tuple, resource: Resource, view_mode: str = None, app: str = None + ) -> cs3apr.OpenInAppURL: """ Open a file in an app, given the resource, view mode (VIEW_MODE_VIEW_ONLY, VIEW_MODE_READ_ONLY, VIEW_MODE_READ_WRITE, VIEW_MODE_PREVIEW), and app name. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: Resource object containing the resource information. :param view_mode: View mode of the app. :param app: App name. @@ -63,21 +63,22 @@ def open_in_app(self, resource: Resource, view_mode: str = None, app: str = None if view_mode: view_mode_type = cs3gw.OpenInAppRequest.ViewMode.Value(view_mode) req = cs3gw.OpenInAppRequest(ref=resource.ref, view_mode=view_mode_type, app=app) - res = self._gateway.OpenInApp(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.OpenInApp(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "open in app", f"{resource.get_file_ref_str()}") self._log.debug(f'msg="Invoked OpenInApp" {resource.get_file_ref_str()} trace="{res.status.trace}"') return res.OpenInAppURL - def list_app_providers(self) -> list[cs3arres.ProviderInfo]: + def list_app_providers(self, auth_token: dict) -> list[cs3arres.ProviderInfo]: """ list_app_providers lists all the app providers. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :return: List of app providers. :raises: AuthenticationException (Operation not permitted) :raises: UnknownException (Unknown error) """ req = cs3arreg.ListAppProvidersRequest() - res = self._gateway.ListAppProviders(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.ListAppProviders(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "list app providers") self._log.debug(f'msg="Invoked ListAppProviders" res_count="{len(res.providers)}" trace="{res.status.trace}"') return res.providers diff --git a/src/auth.py b/cs3client/auth.py similarity index 76% rename from src/auth.py rename to cs3client/auth.py index 94b0714..c4671f4 100644 --- a/src/auth.py +++ b/cs3client/auth.py @@ -3,7 +3,7 @@ Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti. Emails: rasmus.oscar.welander@cern.ch, diogo.castro@cern.ch, giuseppe.lopresti@cern.ch -Last updated: 19/08/2024 +Last updated: 28/08/2024 """ import grpc @@ -15,8 +15,8 @@ from cs3.gateway.v1beta1.gateway_api_pb2_grpc import GatewayAPIStub from cs3.rpc.v1beta1.code_pb2 import CODE_OK -from exceptions.exceptions import AuthenticationException, SecretNotSetException -from config import Config +from cs3client.exceptions.exceptions import AuthenticationException, SecretNotSetException +from cs3client.config import Config class Auth: @@ -40,16 +40,6 @@ def __init__(self, config: Config, log: logging.Logger, gateway: GatewayAPIStub) self._client_secret: str | None = None self._token: str | None = None - def set_token(self, token: str) -> None: - """ - Should be used if the user wishes to set the reva token directly, instead of letting the client - exchange credentials for the token. NOTE that token OR the client secret has to be set when - instantiating the client object. - - :param token: The reva token. - """ - self._token = token - def set_client_secret(self, token: str) -> None: """ Sets the client secret, exists so that the user can change the client secret (e.g. token, password) at runtime, @@ -71,16 +61,13 @@ def get_token(self) -> tuple[str, str]: :raises: SecretNotSetException (neither token or client secret was set) """ - if not Auth._check_token(self._token): - # Check that client secret or token is set - if not self._client_secret and not self._token: - self._log.error("Attempted to authenticate, neither client secret or token was set.") - raise SecretNotSetException("The client secret (e.g. token, passowrd) is not set") - elif not self._client_secret and self._token: - # Case where ONLY a token is provided but it has expired - self._log.error("The provided token have expired") - raise AuthenticationException("The credentials have expired") - # Create an authentication request + try: + Auth.check_token(self._token) + except ValueError: + self._log.error("Attempted to authenticate, neither client secret or token was set.") + raise SecretNotSetException("The client secret (e.g. token, passowrd) is not set") + except AuthenticationException: + # Token has expired, obtain another one. req = AuthenticateRequest( type=self._config.auth_login_type, client_id=self._config.auth_client_id, @@ -116,20 +103,22 @@ def list_auth_providers(self) -> list[str]: return res.types @classmethod - def _check_token(cls, token: str) -> bool: + def check_token(cls, token: str) -> str: """ Checks if the given token is set and valid. :param token: JWT token as a string. - :return: True if the token is valid, False otherwise. + :return tuple: A tuple containing the header key and the token. + :raises: ValueError (Token missing) + :raises: AuthenticationException (Token is expired) """ if not token: - return False + raise ValueError("A token is required") # Decode the token without verifying the signature decoded_token = jwt.decode(jwt=token, algorithms=["HS256"], options={"verify_signature": False}) now = datetime.datetime.now().timestamp() token_expiration = decoded_token.get("exp") if token_expiration and now > token_expiration: - return False + raise AuthenticationException("Token has expired") - return True + return ("x-access-token", token) diff --git a/src/checkpoint.py b/cs3client/checkpoint.py similarity index 83% rename from src/checkpoint.py rename to cs3client/checkpoint.py index 9e4cbdd..a538008 100644 --- a/src/checkpoint.py +++ b/cs3client/checkpoint.py @@ -3,18 +3,18 @@ Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti. Emails: rasmus.oscar.welander@cern.ch, diogo.castro@cern.ch, giuseppe.lopresti@cern.ch -Last updated: 19/08/2024 +Last updated: 28/08/2024 """ from typing import Generator import logging -from auth import Auth import cs3.storage.provider.v1beta1.resources_pb2 as cs3spr import cs3.storage.provider.v1beta1.provider_api_pb2 as cs3spp from cs3.gateway.v1beta1.gateway_api_pb2_grpc import GatewayAPIStub -from config import Config -from statuscodehandler import StatusCodeHandler -from cs3resource import Resource + +from cs3client.config import Config +from cs3client.statuscodehandler import StatusCodeHandler +from cs3client.cs3resource import Resource class Checkpoint: @@ -27,7 +27,6 @@ def __init__( config: Config, log: logging.Logger, gateway: GatewayAPIStub, - auth: Auth, status_code_handler: StatusCodeHandler, ) -> None: """ @@ -36,21 +35,20 @@ def __init__( :param config: Config object containing the configuration parameters. :param log: Logger instance for logging. :param gateway: GatewayAPIStub instance for interacting with CS3 Gateway. - :param auth: An instance of the auth class. :param status_code_handler: An instance of the StatusCodeHandler class. """ self._gateway: GatewayAPIStub = gateway self._log: logging.Logger = log self._config: Config = config - self._auth: Auth = auth self._status_code_handler: StatusCodeHandler = status_code_handler def list_file_versions( - self, resource: Resource, page_token: str = "", page_size: int = 0 + self, auth_token: tuple, resource: Resource, page_token: str = "", page_size: int = 0 ) -> Generator[cs3spr.FileVersion, any, any]: """ List all versions of a file. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: Resource object containing the resource information. :param page_token: Token for pagination. :param page_size: Number of file versions to return. @@ -61,15 +59,18 @@ def list_file_versions( :raises: UnknownException (Unknown error) """ req = cs3spp.ListFileVersionsRequest(ref=resource.ref, page_token=page_token, page_size=page_size) - res = self._gateway.ListFileVersions(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.ListFileVersions(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "list file versions", f"{resource.get_file_ref_str()}") self._log.debug(f'msg="list file versions" {resource.get_file_ref_str()} trace="{res.status.trace}"') return res.versions - def restore_file_version(self, resource: Resource, version_key: str, lock_id: str = None) -> None: + def restore_file_version( + self, auth_token: tuple, resource: Resource, version_key: str, lock_id: str = None + ) -> None: """ Restore a file to a previous version. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: Resource object containing the resource information. :param version_key: Key of the version to restore. :param lock_id: Lock ID of the file (OPTIONAL). @@ -79,7 +80,7 @@ def restore_file_version(self, resource: Resource, version_key: str, lock_id: st :raises: UnknownException (Unknown error) """ req = cs3spp.RestoreFileVersionRequest(ref=resource.ref, key=version_key, lock_id=lock_id) - res = self._gateway.RestoreFileVersion(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.RestoreFileVersion(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "restore file version", f"{resource.get_file_ref_str()}") self._log.debug(f'msg="restore file version" {resource.get_file_ref_str()} trace="{res.status.trace}"') return diff --git a/src/config.py b/cs3client/config.py similarity index 100% rename from src/config.py rename to cs3client/config.py diff --git a/src/cs3client.py b/cs3client/cs3client.py similarity index 82% rename from src/cs3client.py rename to cs3client/cs3client.py index de2e523..43f2450 100644 --- a/src/cs3client.py +++ b/cs3client/cs3client.py @@ -3,22 +3,22 @@ Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti. Emails: rasmus.oscar.welander@cern.ch, diogo.castro@cern.ch, giuseppe.lopresti@cern.ch -Last updated: 19/08/2024 +Last updated: 28/08/2024 """ import grpc import logging import cs3.gateway.v1beta1.gateway_api_pb2_grpc as cs3gw_grpc - from configparser import ConfigParser -from auth import Auth -from file import File -from user import User -from share import Share -from statuscodehandler import StatusCodeHandler -from app import App -from checkpoint import Checkpoint -from config import Config + +from cs3client.auth import Auth +from cs3client.file import File +from cs3client.user import User +from cs3client.share import Share +from cs3client.statuscodehandler import StatusCodeHandler +from cs3client.app import App +from cs3client.checkpoint import Checkpoint +from cs3client.config import Config class CS3Client: @@ -47,13 +47,13 @@ def __init__(self, config: ConfigParser, config_category: str, log: logging.Logg self._gateway: cs3gw_grpc.GatewayAPIStub = cs3gw_grpc.GatewayAPIStub(self.channel) self._status_code_handler: StatusCodeHandler = StatusCodeHandler(self._log, self._config) self.auth: Auth = Auth(self._config, self._log, self._gateway) - self.file: File = File(self._config, self._log, self._gateway, self.auth, self._status_code_handler) - self.user: User = User(self._config, self._log, self._gateway, self.auth, self._status_code_handler) - self.app: App = App(self._config, self._log, self._gateway, self.auth, self._status_code_handler) + self.file: File = File(self._config, self._log, self._gateway, self._status_code_handler) + self.user: User = User(self._config, self._log, self._gateway, self._status_code_handler) + self.app: App = App(self._config, self._log, self._gateway, self._status_code_handler) self.checkpoint: Checkpoint = Checkpoint( - self._config, self._log, self._gateway, self.auth, self._status_code_handler + self._config, self._log, self._gateway, self._status_code_handler ) - self.share = Share(self._config, self._log, self._gateway, self.auth, self._status_code_handler) + self.share = Share(self._config, self._log, self._gateway, self._status_code_handler) def _create_channel(self) -> grpc.Channel: """ diff --git a/src/cs3resource.py b/cs3client/cs3resource.py similarity index 100% rename from src/cs3resource.py rename to cs3client/cs3resource.py diff --git a/src/exceptions/__init__.py b/cs3client/exceptions/__init__.py similarity index 100% rename from src/exceptions/__init__.py rename to cs3client/exceptions/__init__.py diff --git a/src/exceptions/exceptions.py b/cs3client/exceptions/exceptions.py similarity index 100% rename from src/exceptions/exceptions.py rename to cs3client/exceptions/exceptions.py diff --git a/src/file.py b/cs3client/file.py similarity index 83% rename from src/file.py rename to cs3client/file.py index 8abd9b6..74f3e23 100644 --- a/src/file.py +++ b/cs3client/file.py @@ -3,24 +3,23 @@ Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti. Emails: rasmus.oscar.welander@cern.ch, diogo.castro@cern.ch, giuseppe.lopresti@cern.ch -Last updated: 19/08/2024 +Last updated: 28/08/2024 """ import time import logging import http import requests +from typing import Generator import cs3.storage.provider.v1beta1.resources_pb2 as cs3spr import cs3.storage.provider.v1beta1.provider_api_pb2 as cs3sp from cs3.gateway.v1beta1.gateway_api_pb2_grpc import GatewayAPIStub import cs3.types.v1beta1.types_pb2 as types -from config import Config -from typing import Generator -from exceptions.exceptions import AuthenticationException, FileLockedException -from cs3resource import Resource -from auth import Auth -from statuscodehandler import StatusCodeHandler +from cs3client.config import Config +from cs3client.exceptions.exceptions import AuthenticationException, FileLockedException +from cs3client.cs3resource import Resource +from cs3client.statuscodehandler import StatusCodeHandler class File: @@ -29,7 +28,7 @@ class File: """ def __init__( - self, config: Config, log: logging.Logger, gateway: GatewayAPIStub, auth: Auth, + self, config: Config, log: logging.Logger, gateway: GatewayAPIStub, status_code_handler: StatusCodeHandler ) -> None: """ @@ -38,19 +37,18 @@ def __init__( :param config: Config object containing the configuration parameters. :param log: Logger instance for logging. :param gateway: GatewayAPIStub instance for interacting with CS3 Gateway. - :param auth: An instance of the auth class. :param status_code_handler: An instance of the StatusCodeHandler class. """ - self._auth: Auth = auth self._config: Config = config self._log: logging.Logger = log self._gateway: GatewayAPIStub = gateway self._status_code_handler: StatusCodeHandler = status_code_handler - def stat(self, resource: Resource) -> cs3spr.ResourceInfo: + def stat(self, auth_token: tuple, resource: Resource) -> cs3spr.ResourceInfo: """ Stat a file and return the ResourceInfo object. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: resource to stat. :return: cs3.storage.provider.v1beta1.resources_pb2.ResourceInfo (success) :raises: NotFoundException (File not found) @@ -59,7 +57,7 @@ def stat(self, resource: Resource) -> cs3spr.ResourceInfo: """ tstart = time.time() - res = self._gateway.Stat(request=cs3sp.StatRequest(ref=resource.ref), metadata=[self._auth.get_token()]) + res = self._gateway.Stat(request=cs3sp.StatRequest(ref=resource.ref), metadata=[auth_token]) tend = time.time() self._status_code_handler.handle_errors(res.status, "stat", resource.get_file_ref_str()) self._log.info( @@ -68,10 +66,11 @@ def stat(self, resource: Resource) -> cs3spr.ResourceInfo: ) return res.info - def set_xattr(self, resource: Resource, key: str, value: str) -> None: + def set_xattr(self, auth_token: tuple, resource: Resource, key: str, value: str) -> None: """ Set the extended attribute to for a resource. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: resource that has the attribute. :param key: attribute key. :param value: value to set. @@ -83,16 +82,17 @@ def set_xattr(self, resource: Resource, key: str, value: str) -> None: md = cs3spr.ArbitraryMetadata() md.metadata.update({key: value}) # pylint: disable=no-member req = cs3sp.SetArbitraryMetadataRequest(ref=resource.ref, arbitrary_metadata=md) - res = self._gateway.SetArbitraryMetadata(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.SetArbitraryMetadata(request=req, metadata=[auth_token]) # CS3 storages may refuse to set an xattr in case of lock mismatch: this is an overprotection, # as the lock should concern the file's content, not its metadata, however we need to handle that self._status_code_handler.handle_errors(res.status, "set extended attribute", resource.get_file_ref_str()) self._log.debug(f'msg="Invoked setxattr" trace="{res.status.trace}"') - def remove_xattr(self, resource: Resource, key: str) -> None: + def remove_xattr(self, auth_token: tuple, resource: Resource, key: str) -> None: """ Remove the extended attribute . + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: cs3client resource. :param key: key for attribute to remove. :return: None (Success) @@ -101,14 +101,15 @@ def remove_xattr(self, resource: Resource, key: str) -> None: :raises: UnknownException (Unknown error) """ req = cs3sp.UnsetArbitraryMetadataRequest(ref=resource.ref, arbitrary_metadata_keys=[key]) - res = self._gateway.UnsetArbitraryMetadata(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.UnsetArbitraryMetadata(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "remove extended attribute", resource.get_file_ref_str()) self._log.debug(f'msg="Invoked UnsetArbitraryMetaData" trace="{res.status.trace}"') - def rename_file(self, resource: Resource, newresource: Resource) -> None: + def rename_file(self, auth_token: tuple, resource: Resource, newresource: Resource) -> None: """ Rename/move resource to new resource. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: Original resource. :param newresource: New resource. :return: None (Success) @@ -118,14 +119,15 @@ def rename_file(self, resource: Resource, newresource: Resource) -> None: :raises: UnknownException (Unknown Error) """ req = cs3sp.MoveRequest(source=resource.ref, destination=newresource.ref) - res = self._gateway.Move(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.Move(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "rename file", resource.get_file_ref_str()) self._log.debug(f'msg="Invoked Move" trace="{res.status.trace}"') - def remove_file(self, resource: Resource) -> None: + def remove_file(self, auth_token: tuple, resource: Resource) -> None: """ Remove a resource. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: Resource to remove. :return: None (Success) :raises: AuthenticationException (Authentication Failed) @@ -133,14 +135,15 @@ def remove_file(self, resource: Resource) -> None: :raises: UnknownException (Unknown error) """ req = cs3sp.DeleteRequest(ref=resource.ref) - res = self._gateway.Delete(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.Delete(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "remove file", resource.get_file_ref_str()) self._log.debug(f'msg="Invoked Delete" trace="{res.status.trace}"') - def touch_file(self, resource: Resource) -> None: + def touch_file(self, auth_token: tuple, resource: Resource) -> None: """ Create a resource. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: Resource to create. :return: None (Success) :raises: FileLockedException (File is locked) @@ -151,17 +154,18 @@ def touch_file(self, resource: Resource) -> None: ref=resource.ref, opaque=types.Opaque(map={"Upload-Length": types.OpaqueEntry(decoder="plain", value=str.encode("0"))}), ) - res = self._gateway.TouchFile(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.TouchFile(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "touch file", resource.get_file_ref_str()) self._log.debug(f'msg="Invoked TouchFile" trace="{res.status.trace}"') - def write_file(self, resource: Resource, content: str | bytes, size: int) -> None: + def write_file(self, auth_token: tuple, resource: Resource, content: str | bytes, size: int) -> None: """ Write a file using the given userid as access token. The entire content is written and any pre-existing file is deleted (or moved to the previous version if supported), writing a file with size 0 is equivalent to "touch file" and should be used if the implementation does not support touchfile. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: Resource to write content to. :param content: content to write :param size: size of content (optional) @@ -181,7 +185,7 @@ def write_file(self, resource: Resource, content: str | bytes, size: int) -> Non ref=resource.ref, opaque=types.Opaque(map={"Upload-Length": types.OpaqueEntry(decoder="plain", value=str.encode(str(size)))}), ) - res = self._gateway.InitiateFileUpload(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.InitiateFileUpload(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "write file", resource.get_file_ref_str()) tend = time.time() self._log.debug( @@ -197,13 +201,13 @@ def write_file(self, resource: Resource, content: str | bytes, size: int) -> Non "File-Path": resource.file, "File-Size": str(size), "X-Reva-Transfer": protocol.token, - **dict([self._auth.get_token()]), + **dict([auth_token]), } else: headers = { "Upload-Length": str(size), "X-Reva-Transfer": protocol.token, - **dict([self._auth.get_token()]), + **dict([auth_token]), } putres = requests.put( url=protocol.upload_endpoint, @@ -245,10 +249,11 @@ def write_file(self, resource: Resource, content: str | bytes, size: int) -> Non f'elapsedTimems="{(tend - tstart) * 1000:.1f}"' ) - def read_file(self, resource: Resource) -> Generator[bytes, None, None]: + def read_file(self, auth_token: tuple, resource: Resource) -> Generator[bytes, None, None]: """ Read a file. Note that the function is a generator, managed by the app server. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: Resource to read. :return: Generator[Bytes, None, None] (Success) :raises: NotFoundException (Resource not found) @@ -259,7 +264,7 @@ def read_file(self, resource: Resource) -> Generator[bytes, None, None]: # prepare endpoint req = cs3sp.InitiateFileDownloadRequest(ref=resource.ref) - res = self._gateway.InitiateFileDownload(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.InitiateFileDownload(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "read file", resource.get_file_ref_str()) tend = time.time() self._log.debug( @@ -269,7 +274,7 @@ def read_file(self, resource: Resource) -> Generator[bytes, None, None]: # Download try: protocol = [p for p in res.protocols if p.protocol in ["simple", "spaces"]][0] - headers = {"X-Reva-Transfer": protocol.token, **dict([self._auth.get_token()])} + headers = {"X-Reva-Transfer": protocol.token, **dict([auth_token])} fileget = requests.get( url=protocol.download_endpoint, headers=headers, @@ -294,10 +299,11 @@ def read_file(self, resource: Resource) -> Generator[bytes, None, None]: for chunk in data: yield chunk - def make_dir(self, resource: Resource) -> None: + def make_dir(self, auth_token: tuple, resource: Resource) -> None: """ Create a directory. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: Direcotry to create. :return: None (Success) :raises: FileLockedException (File is locked) @@ -305,16 +311,17 @@ def make_dir(self, resource: Resource) -> None: :raises: UnknownException (Unknown error) """ req = cs3sp.CreateContainerRequest(ref=resource.ref) - res = self._gateway.CreateContainer(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.CreateContainer(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "make directory", resource.get_file_ref_str()) self._log.debug(f'msg="Invoked CreateContainer" trace="{res.status.trace}"') def list_dir( - self, resource: Resource + self, auth_token: tuple, resource: Resource ) -> Generator[cs3spr.ResourceInfo, None, None]: """ List the contents of a directory, note that the function is a generator. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource: the directory. :return: Generator[cs3.storage.provider.v1beta1.resources_pb2.ResourceInfo, None, None] (Success) :raises: NotFoundException (Resrouce not found) @@ -322,7 +329,7 @@ def list_dir( :raises: UnknownException (Unknown error) """ req = cs3sp.ListContainerRequest(ref=resource.ref) - res = self._gateway.ListContainer(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.ListContainer(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "list directory", resource.get_file_ref_str()) self._log.debug(f'msg="Invoked ListContainer" trace="{res.status.trace}"') for info in res.infos: diff --git a/src/share.py b/cs3client/share.py similarity index 89% rename from src/share.py rename to cs3client/share.py index 81f1254..37054e8 100644 --- a/src/share.py +++ b/cs3client/share.py @@ -3,16 +3,10 @@ Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti. Emails: rasmus.oscar.welander@cern.ch, diogo.castro@cern.ch, giuseppe.lopresti@cern.ch -Last updated: 19/08/2024 +Last updated: 28/08/2024 """ import logging -from auth import Auth -from cs3resource import Resource -from config import Config -from statuscodehandler import StatusCodeHandler - - import cs3.sharing.collaboration.v1beta1.collaboration_api_pb2 as cs3scapi from cs3.gateway.v1beta1.gateway_api_pb2_grpc import GatewayAPIStub import cs3.sharing.collaboration.v1beta1.resources_pb2 as cs3scr @@ -24,6 +18,10 @@ import google.protobuf.field_mask_pb2 as field_masks import cs3.types.v1beta1.types_pb2 as cs3types +from cs3client.cs3resource import Resource +from cs3client.config import Config +from cs3client.statuscodehandler import StatusCodeHandler + class Share: """ @@ -35,7 +33,6 @@ def __init__( config: Config, log: logging.Logger, gateway: GatewayAPIStub, - auth: Auth, status_code_handler: StatusCodeHandler, ) -> None: """ @@ -44,21 +41,26 @@ def __init__( :param config: Config object containing the configuration parameters. :param log: Logger instance for logging. :param gateway: GatewayAPIStub instance for interacting with CS3 Gateway. - :param auth: An instance of the auth class. :param status_code_handler: An instance of the StatusCodeHandler class. """ self._status_code_handler: StatusCodeHandler = status_code_handler self._gateway: GatewayAPIStub = gateway self._log: logging.Logger = log self._config: Config = config - self._auth: Auth = auth def create_share( - self, resource_info: cs3spr.ResourceInfo, opaque_id: str, idp: str, role: str, grantee_type: str + self, + auth_token: tuple, + resource_info: cs3spr.ResourceInfo, + opaque_id: str, + idp: str, + role: str, + grantee_type: str ) -> cs3scr.Share: """ Create a share for a resource to the user/group with the specified role, using their opaque id and idp. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource_info: Resource info, see file.stat (REQUIRED). :param opaque_id: Opaque group/user id, (REQUIRED). :param idp: Identity provider, (REQUIRED). @@ -74,7 +76,7 @@ def create_share( """ share_grant = Share._create_share_grant(opaque_id, idp, role, grantee_type) req = cs3scapi.CreateShareRequest(resource_info=resource_info, grant=share_grant) - res = self._gateway.CreateShare(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.CreateShare(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors( res.status, "create share", f'opaque_id="{opaque_id}" resource_id="{resource_info.id}"' ) @@ -85,11 +87,12 @@ def create_share( return res.share def list_existing_shares( - self, filter_list: list[cs3scr.Filter] = None, page_size: int = 0, page_token: str = None + self, auth_token: tuple, filter_list: list[cs3scr.Filter] = None, page_size: int = 0, page_token: str = None ) -> list[cs3scr.Share]: """ List shares based on a filter. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param filter: Filter object to filter the shares, see create_share_filter. :param page_size: Number of shares to return in a page, defaults to 0, server decides. :param page_token: Token to get to a specific page. @@ -98,7 +101,7 @@ def list_existing_shares( :raises: UnknownException (Unknown error) """ req = cs3scapi.ListSharesRequest(filters=filter_list, page_size=page_size, page_token=page_token) - res = self._gateway.ListExistingShares(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.ListExistingShares(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "list existing shares", f'filter="{filter_list}"') self._log.debug( f'msg="Invoked ListExistingShares" filter="{filter_list}" res_count="{len(res.share_infos)}' @@ -106,11 +109,12 @@ def list_existing_shares( ) return (res.share_infos, res.next_page_token) - def get_share(self, opaque_id: str = None, share_key: cs3scr.ShareKey = None) -> cs3scr.Share: + def get_share(self, auth_token: tuple, opaque_id: str = None, share_key: cs3scr.ShareKey = None) -> cs3scr.Share: """ Get a share by its opaque id or share key (combination of resource_id, grantee and owner), one of them is required. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param opaque_id: Opaque share id (SEMI-OPTIONAL). :param share_key: Share key, see ShareKey definition in cs3apis (SEMI-OPTIONAL). :return: Share object. @@ -127,7 +131,7 @@ def get_share(self, opaque_id: str = None, share_key: cs3scr.ShareKey = None) -> else: raise ValueError("opaque_id or share_key is required") - res = self._gateway.GetShare(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.GetShare(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors( res.status, "get share", f'opaque_id/share_key="{opaque_id if opaque_id else share_key}"' ) @@ -137,11 +141,12 @@ def get_share(self, opaque_id: str = None, share_key: cs3scr.ShareKey = None) -> ) return res.share - def remove_share(self, opaque_id: str = None, share_key: cs3scr.ShareKey = None) -> None: + def remove_share(self, auth_token: tuple, opaque_id: str = None, share_key: cs3scr.ShareKey = None) -> None: """ Remove a share by its opaque id or share key (combination of resource_id, grantee and owner), one of them is required. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param opaque_id: Opaque share id (SEMI-OPTIONAL). :param share_key: Share key, see ShareKey definition in cs3apis (SEMI-OPTIONAL). :return: None @@ -157,7 +162,7 @@ def remove_share(self, opaque_id: str = None, share_key: cs3scr.ShareKey = None) req = cs3scapi.RemoveShareRequest(ref=cs3scr.ShareReference(key=share_key)) else: raise ValueError("opaque_id or share_key is required") - res = self._gateway.RemoveShare(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.RemoveShare(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors( res.status, "remove share", f'opaque_id/share_key="{opaque_id if opaque_id else share_key}"' ) @@ -168,11 +173,16 @@ def remove_share(self, opaque_id: str = None, share_key: cs3scr.ShareKey = None) return def update_share( - self, role: str, opaque_id: str = None, share_key: cs3scr.ShareKey = None, display_name: str = None + self, auth_token: tuple, + role: str, + opaque_id: str = None, + share_key: cs3scr.ShareKey = None, + display_name: str = None ) -> cs3scr.Share: """ Update a share by its opaque id. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param opaque_id: Opaque share id. (SEMI-OPTIONAL). :param share_key: Share key, see ShareKey definition in cs3apis (SEMI-OPTIONAL). :param role: Role to update the share, VIEWER or EDITOR (REQUIRED). @@ -195,7 +205,7 @@ def update_share( raise ValueError("opaque_id or share_key is required") req = cs3scapi.UpdateShareRequest(ref=ref, field=update) - res = self._gateway.UpdateShare(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.UpdateShare(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors( res.status, "update share", f'opaque_id/share_key="{opaque_id if opaque_id else share_key}"' ) @@ -206,12 +216,13 @@ def update_share( return res.share def list_received_existing_shares( - self, filter_list: list = None, page_size: int = 0, page_token: str = None + self, auth_token: tuple, filter_list: list = None, page_size: int = 0, page_token: str = None ) -> list: """ List received existing shares. NOTE: Filters for received shares are not yet implemented (14/08/2024) + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param filter: Filter object to filter the shares, see create_share_filter. :param page_size: Number of shares to return in a page, defaults to 0, server decides. :param page_token: Token to get to a specific page. @@ -220,7 +231,7 @@ def list_received_existing_shares( :raises: UnknownException (Unknown error) """ req = cs3scapi.ListReceivedSharesRequest(filters=filter_list, page_size=page_size, page_token=page_token) - res = self._gateway.ListExistingReceivedShares(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.ListExistingReceivedShares(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "list received existing shares", f'filter="{filter_list}"') self._log.debug( f'msg="Invoked ListExistingReceivedShares" filter="{filter_list}" res_count="{len(res.share_infos)}"' @@ -228,11 +239,14 @@ def list_received_existing_shares( ) return (res.share_infos, res.next_page_token) - def get_received_share(self, opaque_id: str = None, share_key: cs3scr.ShareKey = None) -> cs3scr.ReceivedShare: + def get_received_share( + self, auth_token: tuple, opaque_id: str = None, share_key: cs3scr.ShareKey = None + ) -> cs3scr.ReceivedShare: """ Get a received share by its opaque id or share key (combination of resource_id, grantee and owner), one of them is required. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param opaque_id: Opaque share id. (SEMI-OPTIONAL). :param share_key: Share key, see ShareKey definition in cs3apis (SEMI-OPTIONAL). :return: ReceivedShare object. @@ -248,7 +262,7 @@ def get_received_share(self, opaque_id: str = None, share_key: cs3scr.ShareKey = req = cs3scapi.GetReceivedShareRequest(ref=cs3scr.ShareReference(key=share_key)) else: raise ValueError("opaque_id or share_key is required") - res = self._gateway.GetReceivedShare(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.GetReceivedShare(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors( res.status, "get received share", f'opaque_id/share_key="{opaque_id if opaque_id else share_key}"' ) @@ -259,11 +273,12 @@ def get_received_share(self, opaque_id: str = None, share_key: cs3scr.ShareKey = return res.share def update_received_share( - self, received_share: cs3scr.ReceivedShare, state: str = "SHARE_STATE_ACCEPTED" + self, auth_token: tuple, received_share: cs3scr.ReceivedShare, state: str = "SHARE_STATE_ACCEPTED" ) -> cs3scr.ReceivedShare: """ Update the state of a received share (SHARE_STATE_ACCEPTED, SHARE_STATE_ACCEPTED, SHARE_STATE_REJECTED). + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param recieved_share: ReceivedShare object. :param state: Share state to update to, defaults to SHARE_STATE_ACCEPTED, (REQUIRED). :return: Updated ReceivedShare object. @@ -283,7 +298,7 @@ def update_received_share( ), update_mask=field_masks.FieldMask(paths=["state"]), ) - res = self._gateway.UpdateReceivedShare(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.UpdateReceivedShare(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors( res.status, "update received share", f'opaque_id="{received_share.share.id.opaque_id}"' ) @@ -295,6 +310,7 @@ def update_received_share( def create_public_share( self, + auth_token: tuple, resource_info: cs3spr.ResourceInfo, role: str, password: str = None, @@ -307,6 +323,7 @@ def create_public_share( """ Create a public share. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param resource_info: Resource info, see file.stat (REQUIRED). :param role: Role to assign to the grantee, VIEWER or EDITOR (REQUIRED) :param password: Password to access the share. @@ -335,17 +352,18 @@ def create_public_share( notify_uploads_extra_recipients=notify_uploads_extra_recipients, ) - res = self._gateway.CreatePublicShare(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.CreatePublicShare(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "create public share", f'resource_id="{resource_info.id}"') self._log.debug(f'msg="Invoked CreatePublicShare" resource_id="{resource_info.id}" trace="{res.status.trace}"') return res.share def list_existing_public_shares( - self, filter_list: list = None, page_size: int = 0, page_token: str = None, sign: bool = None + self, auth_token: tuple, filter_list: list = None, page_size: int = 0, page_token: str = None, sign: bool = None ) -> list: """ List existing public shares. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param filter: Filter object to filter the shares, see create_public_share_filter. :param page_size: Number of shares to return in a page, defaults to 0 and then the server decides. :param page_token: Token to get to a specific page. @@ -358,7 +376,7 @@ def list_existing_public_shares( req = cs3slapi.ListPublicSharesRequest( filters=filter_list, page_size=page_size, page_token=page_token, sign=sign ) - res = self._gateway.ListExistingPublicShares(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.ListExistingPublicShares(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "list existing public shares", f'filter="{filter_list}"') self._log.debug( f'msg="Invoked ListExistingPublicShares" filter="{filter_list}" res_count="{len(res.share_infos)}" ' @@ -366,10 +384,13 @@ def list_existing_public_shares( ) return (res.share_infos, res.next_page_token) - def get_public_share(self, opaque_id: str = None, token: str = None, sign: bool = False) -> cs3slr.PublicShare: + def get_public_share( + self, auth_token: tuple, opaque_id: str = None, token: str = None, sign: bool = False + ) -> cs3slr.PublicShare: """ Get a public share by its opaque id or token, one of them is required. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param opaque_id: Opaque share id (SEMI-OPTIONAL). :param share_token: Share token (SEMI-OPTIONAL). :param sign: if the signature should be included in the share. @@ -387,7 +408,7 @@ def get_public_share(self, opaque_id: str = None, token: str = None, sign: bool else: raise ValueError("token or opaque_id is required") req = cs3slapi.GetPublicShareRequest(ref=ref, sign=sign) - res = self._gateway.GetPublicShare(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.GetPublicShare(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors( res.status, "get public share", f'opaque_id/token="{opaque_id if opaque_id else token}"' ) @@ -399,6 +420,7 @@ def get_public_share(self, opaque_id: str = None, token: str = None, sign: bool def update_public_share( self, + auth_token: tuple, type: str, role: str, opaque_id: str = None, @@ -415,6 +437,7 @@ def update_public_share( however, other parameters are optional. Note that only the type of update specified will be applied. The role will only change if type is TYPE_PERMISSIONS. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param type: Type of update to perform TYPE_PERMISSIONS, TYPE_PASSWORD, TYPE_EXPIRATION, TYPE_DISPLAYNAME, TYPE_DESCRIPTION, TYPE_NOTIFYUPLOADS, TYPE_NOTIFYUPLOADSEXTRARECIPIENTS (REQUIRED). :param role: Role to assign to the grantee, VIEWER or EDITOR (REQUIRED). @@ -451,7 +474,7 @@ def update_public_share( password=password, ) req = cs3slapi.UpdatePublicShareRequest(ref=ref, update=update) - res = self._gateway.UpdatePublicShare(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.UpdatePublicShare(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors( res.status, "update public share", @@ -463,10 +486,11 @@ def update_public_share( ) return res.share - def remove_public_share(self, token: str = None, opaque_id: str = None) -> None: + def remove_public_share(self, auth_token: tuple, token: str = None, opaque_id: str = None) -> None: """ Remove a public share by its token or opaque id, one of them is required. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param token: Share token (SEMI-OPTIONAL). :param opaque_id: Opaque share id (SEMI-OPTIONAL). :return: None @@ -481,7 +505,7 @@ def remove_public_share(self, token: str = None, opaque_id: str = None) -> None: raise ValueError("token or opaque_id is required") req = cs3slapi.RemovePublicShareRequest(ref=ref) - res = self._gateway.RemovePublicShare(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.RemovePublicShare(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors( res.status, "remove public share", f'opaque_id/token="{opaque_id if opaque_id else token}"' ) diff --git a/src/statuscodehandler.py b/cs3client/statuscodehandler.py similarity index 97% rename from src/statuscodehandler.py rename to cs3client/statuscodehandler.py index 2a63907..933986c 100644 --- a/src/statuscodehandler.py +++ b/cs3client/statuscodehandler.py @@ -6,13 +6,14 @@ Last updated: 19/08/2024 """ -from exceptions.exceptions import AuthenticationException, NotFoundException, \ - UnknownException, AlreadyExistsException, FileLockedException, UnimplementedException import logging -from config import Config import cs3.rpc.v1beta1.code_pb2 as cs3code import cs3.rpc.v1beta1.status_pb2 as cs3status +from cs3client.exceptions.exceptions import AuthenticationException, NotFoundException, \ + UnknownException, AlreadyExistsException, FileLockedException, UnimplementedException +from cs3client.config import Config + class StatusCodeHandler: def __init__(self, log: logging.Logger, config: Config) -> None: diff --git a/src/user.py b/cs3client/user.py similarity index 91% rename from src/user.py rename to cs3client/user.py index 2d64d4b..5e896de 100644 --- a/src/user.py +++ b/cs3client/user.py @@ -3,16 +3,16 @@ Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti. Emails: rasmus.oscar.welander@cern.ch, diogo.castro@cern.ch, giuseppe.lopresti@cern.ch -Last updated: 19/08/2024 +Last updated: 28/08/2024 """ import logging -from auth import Auth -from config import Config import cs3.identity.user.v1beta1.resources_pb2 as cs3iur import cs3.identity.user.v1beta1.user_api_pb2 as cs3iu from cs3.gateway.v1beta1.gateway_api_pb2_grpc import GatewayAPIStub -from statuscodehandler import StatusCodeHandler + +from cs3client.config import Config +from cs3client.statuscodehandler import StatusCodeHandler class User: @@ -25,7 +25,6 @@ def __init__( config: Config, log: logging.Logger, gateway: GatewayAPIStub, - auth: Auth, status_code_handler: StatusCodeHandler, ) -> None: """ @@ -35,7 +34,6 @@ def __init__( :param gateway: GatewayAPIStub instance for interacting with CS3 Gateway. :param auth: An instance of the auth class. """ - self._auth: Auth = auth self._log: logging.Logger = log self._gateway: GatewayAPIStub = gateway self._config: Config = config @@ -92,10 +90,11 @@ def get_user_groups(self, idp, opaque_id) -> list[str]: self._log.debug(f'msg="Invoked GetUserGroups" opaque_id="{opaque_id}" trace="{res.status.trace}"') return res.groups - def find_users(self, filter) -> list[cs3iur.User]: + def find_users(self, auth_token: tuple, filter) -> list[cs3iur.User]: """ Find a user based on a filter. + :param auth_token: tuple in the form ('x-access-token', (see auth.get_token/auth.check_token) :param filter: Filter to search for. :return: a list of user(s). :raises: NotFoundException (User not found) @@ -103,7 +102,7 @@ def find_users(self, filter) -> list[cs3iur.User]: :raises: UnknownException (Unknown error) """ req = cs3iu.FindUsersRequest(filter=filter, skip_fetching_user_groups=True) - res = self._gateway.FindUsers(request=req, metadata=[self._auth.get_token()]) + res = self._gateway.FindUsers(request=req, metadata=[auth_token]) self._status_code_handler.handle_errors(res.status, "find users") self._log.debug(f'msg="Invoked FindUsers" filter="{filter}" trace="{res.status.trace}"') return res.users