Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation #1

Merged
merged 5 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ coverage.xml
.pytest_cache/
cover/

# vscode settings
.vscode/

# Translations
*.mo
*.pot
Expand Down
49 changes: 49 additions & 0 deletions examples/default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# example configuration file for the CS3client.
#
# Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti
# Emails: [email protected], [email protected], [email protected]
# Last updated: 29/07/2024

[cs3client]

# Required
host = localhost:19000
# Optional, defaults to 4194304
chunk_size = 4194304
# Optional, defaults to 10
grpc_timeout = 10
# Optional, defaults to 10
http_timeout = 10

# Optional, defaults to True
tus_enabled = False

# Optional, defaults to True
ssl_enabled = False
# Optional, defaults to True
ssl_verify = False
# Optional, defaults to an empty string
ssl_client_cert = test_client_cert
# Optional, defaults to an empty string
ssl_client_key = test_client_key
# Optional, defaults to an empty string
ssl_ca_cert = test_ca_cert

# Optinal, defaults to an empty string
auth_client_id = einstein
# Optional, defaults to basic
auth_login_type = basic

# For the future lock implementation

# Optional, defaults to False
# This configuration is used to enable/disable the fallback mechanism
# if the locks are not implemented in the storage provider
lock_by_setting_attr = False
# This configuration is used to enable/disable the fallback mechanism
# if the locks are not implemented in the storage provider
# Optional, defaults to False
lock_not_impl = False
# Optional, defaults to 1800
lock_expiration = 1800

125 changes: 125 additions & 0 deletions examples/file_api_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""
example.py

Example script to demonstrate the usage of the CS3Client class.
Start with an empty directory and you should end up with a directory structure like this:

test_directory1
test_directory2
test_directory3
rename_file.txt (containing "Hello World")
text_file.txt


Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti.
Emails: [email protected], [email protected], [email protected]
Last updated: 29/07/2024
"""

import logging
import configparser
from cs3client import CS3Client
from cs3resource import Resource

config = configparser.ConfigParser()
with open("default.conf") as fdef:
config.read_file(fdef)
# log
log = logging.getLogger(__name__)

client = CS3Client(config, "cs3client", log)
client.auth.set_client_secret("relativity")

# Authentication
print(client.auth.get_token())

res = None

# mkdir
for i in range(1, 4):
directory_resource = Resource.from_file_ref_and_endpoint(f"/eos/user/r/rwelande/test_directory{i}")
res = client.file.make_dir(directory_resource)
if res is not None:
print(res)

# touchfile
touch_resource = Resource.from_file_ref_and_endpoint("/eos/user/r/rwelande/touch_file.txt")
text_resource = Resource.from_file_ref_and_endpoint("/eos/user/r/rwelande/text_file.txt")
res = client.file.touch_file(touch_resource)
res = client.file.touch_file(text_resource)

if res is not None:
print(res)

# setxattr
resource = Resource.from_file_ref_and_endpoint("/eos/user/r/rwelande/text_file.txt")
res = client.file.set_xattr(resource, "iop.wopi.lastwritetime", str(1720696124))

if res is not None:
print(res)

# rmxattr
res = client.file.remove_xattr(resource, "iop.wopi.lastwritetime")

if res is not None:
print(res)

# stat
res = client.file.stat(text_resource)

if res is not None:
print(res)

# removefile
res = client.file.remove_file(touch_resource)

if res is not None:
print(res)

res = client.file.touch_file(touch_resource)

# rename
rename_resource = Resource.from_file_ref_and_endpoint("/eos/user/r/rwelande/rename_file.txt")
res = client.file.rename_file(resource, rename_resource)

if res is not None:
print(res)

# writefile
content = b"Hello World"
size = len(content)
res = client.file.write_file(rename_resource, content, size)

if res is not None:
print(res)

# rmdir (same as deletefile)
res = client.file.remove_file(directory_resource)

if res is not None:
print(res)

# listdir
list_directory_resource = Resource.from_file_ref_and_endpoint("/eos/user/r/rwelande")
res = client.file.list_dir(list_directory_resource)

first_item = next(res, None)
if first_item is not None:
print(first_item)
for item in res:
print(item)
else:
print("empty response")

# readfile
file_res = client.file.read_file(rename_resource)
content = b""
try:
for chunk in file_res:
if isinstance(chunk, Exception):
raise chunk
content += chunk
print(content.decode("utf-8"))
except Exception as e:
print(f"An error occurred: {e}")
print(e)
30 changes: 30 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
setup.py

setup file for the cs3client package.

Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti.
Emails: [email protected], [email protected], [email protected]
Last updated: 26/07/2024
"""

from setuptools import setup, find_packages

setup(
name="cs3client",
version="0.1",
author="Rasmus Welander, Diogo Castro, Giuseppe Lo Presti",
package_dir={"": "src"},
packages=find_packages(where="src"),
py_modules=["cs3client"],
rawe0 marked this conversation as resolved.
Show resolved Hide resolved
install_requires=[
"grpcio>=1.47.0",
"grpcio-tools>=1.47.0",
"pyOpenSSL",
"requests",
"cs3apis>=0.1.dev101",
"PyJWT",
"protobuf",
"cryptography",
],
)
Empty file added src/__init__.py
Empty file.
137 changes: 137 additions & 0 deletions src/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""
auth.py

Authors: Rasmus Welander, Diogo Castro, Giuseppe Lo Presti.
Emails: [email protected], [email protected], [email protected]
Last updated: 29/07/2024
"""

from typing import List
import grpc
import jwt
import datetime
import logging
import cs3.gateway.v1beta1.gateway_api_pb2 as gw
from cs3.auth.registry.v1beta1.registry_api_pb2 import ListAuthProvidersRequest
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


class Auth:
"""
Auth class to handle authentication and token validation with CS3 Gateway API.
"""

def __init__(self, config: Config, log: logging.Logger, gateway: GatewayAPIStub) -> None:
"""
Initializes the Auth class with configuration, logger, and gateway stub,
NOTE that token OR the client secret has to be set when instantiating the auth object.

:param config: Config object containing the configuration parameters.
:param log: Logger instance for logging.
:param gateway: GatewayAPIStub instance for interacting with CS3 Gateway.
"""
self._gateway: GatewayAPIStub = gateway
self._log: logging.Logger = log
self._config: Config = config
# The user should be able to change the client secret (e.g. token) at runtime
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,
without having to create a new Auth object. NOTE that token OR the client secret has to be set when
instantiating the client object.

:param token: Auth token/password.
"""
self._client_secret = token

rawe0 marked this conversation as resolved.
Show resolved Hide resolved
def get_token(self) -> tuple[str, str]:
"""
Attempts to get a valid authentication token. If the token is not valid, a new token is requested
if the client secret is set, if only the token is set then an exception will be thrown stating that
the credentials have expired.

:return tuple: A tuple containing the header key and the token.
May throw AuthenticationException (token expired, or failed to authenticate)
or 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("")
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
req = gw.AuthenticateRequest(
type=self._config.auth_login_type,
client_id=self._config.auth_client_id,
client_secret=self._client_secret,
)
# Send the authentication request to the CS3 Gateway
res = self._gateway.Authenticate(req)

if res.status.code != CODE_OK:
self._log.error(
f"Failed to authenticate user {self._config.auth_client_id}, error: {res.status.message}"
)
raise AuthenticationException(
f"Failed to authenticate user {self._config.auth_client_id}, error: {res.status.message}"
)
self._token = res.token
return ("x-access-token", self._token)

def list_auth_providers(self) -> List[str]:
"""
list authentication providers

:return: a list of the supported authentication types
May return ConnectionError (Could not connect to host)
"""
try:
res = self._gateway.ListAuthProviders(request=ListAuthProvidersRequest())
if res.status.code != CODE_OK:
self._log.error(f"List auth providers request failed, error: {res.status.message}")
raise Exception(res.status.message)
except grpc.RpcError as e:
self._log.error("List auth providers request failed")
raise ConnectionError(e)
return res.types

@classmethod
def _check_token(cls, token: str) -> bool:
"""
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.
"""
if not token:
return False
# 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

return True
Loading