diff --git a/apiserver/main.py b/apiserver/main.py index d74e65ff148fdf58574e37b7cda4295610a46f80..4576b6d963969dbb500d5d9ed801e659f6182e76 100644 --- a/apiserver/main.py +++ b/apiserver/main.py @@ -1,18 +1,17 @@ +import logging from datetime import timedelta from enum import Enum -from typing import Dict, Optional -from fastapi import FastAPI, HTTPException, status, Request +from fastapi import FastAPI, HTTPException, Request, status from fastapi.param_functions import Depends from fastapi.responses import JSONResponse from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from .config import ApiserverSettings -from .security import (ACCESS_TOKEN_EXPIRES_MINUTES, AbstractDBInterface, - JsonDBInterface, Token, User, authenticate_user, - create_access_token, get_current_user) -from .storage import (AbstractLocationDataStorageAdapter, - JsonFileStorageAdapter, LocationData, LocationDataType) +from .security import (ACCESS_TOKEN_EXPIRES_MINUTES, JsonDBInterface, Token, + User, authenticate_user, create_access_token, + get_current_user) +from .storage import JsonFileStorageAdapter, LocationData, LocationDataType class ReservedPaths(str, Enum): @@ -46,7 +45,7 @@ def my_user(token=Depends(oauth2_scheme)): def my_auth(form_data: OAuth2PasswordRequestForm = Depends()): return authenticate_user(userdb, form_data.username, form_data.password) - + @app.get("/") async def get_types(): # list types of data locations, currently datasets @@ -95,16 +94,15 @@ async def add_dataset(location_data_type: LocationDataType, async def get_specific_dataset(location_data_type: LocationDataType, dataset_id: str): # returns all information about a specific dataset, identified by id return adapter.get_details(location_data_type, dataset_id) - + @app.put("/{location_data_type}/{dataset_id}") async def update_specific_dataset(location_data_type: LocationDataType, dataset_id: str, dataset: LocationData, user: User = Depends(my_user)): # update the information about a specific dataset, identified by id - return adapter.update_details(location_data_type, dataset_id, dataset, user.username) - + @app.delete("/{location_data_type}/{dataset_id}") async def delete_specific_dataset(location_data_type: LocationDataType, @@ -113,10 +111,11 @@ async def delete_specific_dataset(location_data_type: LocationDataType, # delete a specific dataset # TODO: 404 is the right answer? 204 could also be the right one return adapter.delete(location_data_type, dataset_id, user.username) - + @app.exception_handler(FileNotFoundError) -async def not_found_handler(request: Request, exc: FileNotFoundError): +async def not_found_handler(request: Request, ex: FileNotFoundError): oid=request.path_params.get('dataset_id', '') - return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, - content={'message':f"Object {oid} does not exist"}) \ No newline at end of file + logging.error("File not found translated %s", ex) + return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, + content={'message':f"Object {oid} does not exist"}) diff --git a/apiserver/security/user.py b/apiserver/security/user.py index a8880fc1dc1393ca272237538c360500b3b492c7..8377766bc72d61871c85627ff1041594c1cb98fb 100644 --- a/apiserver/security/user.py +++ b/apiserver/security/user.py @@ -1,3 +1,4 @@ +import logging import abc import json import os @@ -5,7 +6,7 @@ import warnings from datetime import datetime, timedelta from typing import List, Optional -from fastapi import Depends, HTTPException, status +from fastapi import HTTPException, status from passlib.context import CryptContext from pydantic import BaseModel @@ -56,11 +57,11 @@ class AbstractDBInterface(metaclass=abc.ABCMeta): class JsonDBInterface(AbstractDBInterface): - + def __init__(self, settings: ApiserverSettings): - print(f"Recreating userdb {settings}") - self.filePath = settings.userdb_path - if not (os.path.exists(self.filePath) and os.path.isfile(self.filePath)): + logging.info("Recreating userdb %s", settings) + self.file_path = settings.userdb_path + if not (os.path.exists(self.file_path) and os.path.isfile(self.file_path)): # create empty json self.__save_all({}) else: @@ -68,11 +69,11 @@ class JsonDBInterface(AbstractDBInterface): _ = self.__read_all() def __read_all(self): - with open(self.filePath, 'r') as f: + with open(self.file_path, 'r') as f: return json.load(f) - + def __save_all(self, data): - with open(self.filePath, 'w') as f: + with open(self.file_path, 'w') as f: json.dump(data, f) def list(self): @@ -83,15 +84,15 @@ class JsonDBInterface(AbstractDBInterface): data = self.__read_all() if username not in data: return None - + return UserInDB(**data[username]) def add(self, user: UserInDB): data = self.__read_all() - if user.username in data: + if user.username in data: raise Exception(f"User {user.username} already exists!") - - data[user.username] = user.dict() + + data[user.username] = user.dict() self.__save_all(data=data) def delete(self, username: str): @@ -125,21 +126,22 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = timedel expire = datetime.utcnow() + expires_delta to_encode.update({"exp": expire}) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - + credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, +) + + def get_current_user(token: str, userdb: AbstractDBInterface): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") - if (username is None) or ((user:=userdb.get(username)) is None): + if (username is None) or ((user := userdb.get(username)) is None): raise credentials_exception - + return user except JWTError: - raise credentials_exception + raise credentials_exception from JWTError diff --git a/apiserver/storage/JsonFileStorageAdapter.py b/apiserver/storage/JsonFileStorageAdapter.py index f1334c4a68fb145fde17bb1044a9c7e0ccf17e30..bdae20d03f860377e0d0a8df05d7edaa255b4318 100644 --- a/apiserver/storage/JsonFileStorageAdapter.py +++ b/apiserver/storage/JsonFileStorageAdapter.py @@ -15,16 +15,24 @@ class StoredData(BaseModel): actualData: LocationData users: List[str] +def load_object(path): + return StoredData.parse_file(path) + +def get_unique_id(path: str) -> str: + oid = str(uuid.uuid4()) + while os.path.exists(os.path.join(path, oid)): + oid = str(uuid.uuid4()) + return oid class JsonFileStorageAdapter(AbstractLocationDataStorageAdapter): """ This stores LocationData via the StoredData Object as json files - These Jsonfiles then contain the actualData, as well as the users with permissions - for this LocationData all users have full permission to to anything with - this dataobject, uncluding removing their own access (this might trigger a + These Jsonfiles then contain the actualData, as well as the users with permissions + for this LocationData all users have full permission to to anything with + this dataobject, uncluding removing their own access (this might trigger a confirmation via the frontend, but this is not enforced via the api) - IMPORTANT: The adapter does not check for authentication or authorization, + IMPORTANT: The adapter does not check for authentication or authorization, it should only be invoked if the permissions have been checked """ @@ -36,7 +44,7 @@ class JsonFileStorageAdapter(AbstractLocationDataStorageAdapter): def __setup_path(self, value: str) -> str: localpath = os.path.join(self.data_dir, value) - if not (os.path.isdir(localpath)): + if not os.path.isdir(localpath): os.mkdir(localpath) return localpath @@ -48,43 +56,34 @@ class JsonFileStorageAdapter(AbstractLocationDataStorageAdapter): f"The requested object ({oid}) does not exist.") return fullpath - def __load_object(self, path): - return StoredData.parse_file(path) - - def __get_unique_id(self, path: str) -> str: - oid = str(uuid.uuid4()) - while (os.path.exists(os.path.join(path, oid))): - oid = str(uuid.uuid4()) - return oid - def get_list(self, n_type: LocationDataType) -> List: local_path = self.__setup_path(n_type.value) - retList = [] + ret = [] for f in os.listdir(local_path): p = os.path.join(local_path, f) if not os.path.isfile(p): continue - data = self.__load_object(p) - retList.append((data.actualData.name, f)) - return retList + data = load_object(p) + ret.append((data.actualData.name, f)) + return ret def add_new(self, n_type: LocationDataType, data: LocationData, user_name: str): localpath = self.__setup_path(value=n_type.value) - oid = self.__get_unique_id(path=localpath) - toStore = StoredData(users=[user_name], actualData=data) + oid = get_unique_id(path=localpath) + to_store = StoredData(users=[user_name], actualData=data) with open(os.path.join(localpath, oid), 'w') as json_file: - json.dump(toStore.dict(), json_file) + json.dump(to_store.dict(), json_file) return (oid, data) def get_details(self, n_type: LocationDataType, oid: str): full_path = self.__get_object_path(value=n_type.value, oid=oid) - obj = self.__load_object(path=full_path) + obj = load_object(path=full_path) return obj.actualData def update_details(self, n_type: LocationDataType, oid: str, data: LocationData, usr: str): # TODO: usr is ignored here? full_path = self.__get_object_path(value=n_type.value, oid=oid) - obj = self.__load_object(path=full_path) + obj = load_object(path=full_path) obj.actualData = data with open(full_path, 'w') as f: @@ -93,17 +92,19 @@ class JsonFileStorageAdapter(AbstractLocationDataStorageAdapter): return (oid, data) def delete(self, n_type: LocationDataType, oid: str, usr: str): - fullpath = self.__get_object_path(value=n_type.value, oid=oid) - os.remove(fullpath) + full_path = self.__get_object_path(value=n_type.value, oid=oid) + os.remove(full_path) - def get_owner(self, type: LocationDataType, oid: str): + def get_owner(self, n_type: LocationDataType, oid: str): raise NotImplementedError() - def check_perm(self, type: LocationDataType, oid: str, usr: str): + def check_perm(self, n_type: LocationDataType, oid: str, usr: str): raise NotImplementedError() - def add_perm(self, type: LocationDataType, oid: str, authUsr: str, newUser: str): + def add_perm(self, n_type: LocationDataType, oid: str, usr: str): + """add user to file perm""" raise NotImplementedError() - def rm_perm(self, type: LocationDataType, oid: str, usr: str, rmUser: str): + def rm_perm(self, n_type: LocationDataType, oid: str, usr: str): + """remove user from file perm""" raise NotImplementedError() diff --git a/apiserver/storage/LocationStorage.py b/apiserver/storage/LocationStorage.py index 1ffb220163c72a4d182366e382361bef0bfe1e72..03b4bedef3b79cb48811f5cb4b20a161a9dba28a 100644 --- a/apiserver/storage/LocationStorage.py +++ b/apiserver/storage/LocationStorage.py @@ -17,58 +17,65 @@ class LocationData(BaseModel): class AbstractLocationDataStorageAdapter: - ''' - This is an abstract storage adapter for storing information about datasets, - storage targets and similar things. It can easily be expanded to also store - other data (that has roughly similar metadata), just by expanding + """ + This is an abstract storage adapter for storing information about datasets, + storage targets and similar things. It can easily be expanded to also store + other data (that has roughly similar metadata), just by expanding the `LocationDataType` Enum. - In general, all data is public. This means, that the adapter does not + In general, all data is public. This means, that the adapter does not do any permission checking, except when explicitly asked via the `checkPerm` - function. The caller therefore has to manually decide when to check for - permissions, and not call any function unless it is already authorized + function. The caller therefore has to manually decide when to check for + permissions, and not call any function unless it is already authorized (or does not need any authorization). - The usr: str (the user id) that is required for several functions, is - a unique and immutable string, that identifies the user. This can be a - verified email or a user name. The management of authentication etc. is + The usr: str (the user id) that is required for several functions, is + a unique and immutable string, that identifies the user. This can be a + verified email or a user name. The management of authentication etc. is done by the caller, this adapter assumes that the user id fulfills the criteria. Permissions are stored as a list of user ids, and every id is authorized for full access. - ''' + """ def get_list(self, n_type: LocationDataType) -> List: - # get a list of all LocationData Elements with the provided type, as pairs of {name : id} + """Get a list of all LocationData Elements with the provided type, as pairs of {name : id}""" + raise NotImplementedError() + + def add_new(self, n_type: LocationDataType, data: LocationData, user_name: str): + """ + add a new element of the provided type, assign and return the id and + the new data as {id : LocationData} + """ - def add_new(self, n_type: LocationDataType, data: LocationData, usr: str): - # add a new element of the provided type, assign and return the id and - # the new data as {id : LocationData} raise NotImplementedError() def get_details(self, n_type: LocationDataType, oid: str): - # return the LocationData of the requested object (identified by oid and type) + """ return the LocationData of the requested object (identified by oid and type)""" raise NotImplementedError() def update_details(self, n_type: LocationDataType, oid: str, data: LocationData, usr: str): - # change the details of the requested object, return {oid : newData} + """ change the details of the requested object, return {oid : newData}""" raise NotImplementedError() def delete(self, n_type: LocationDataType, oid: str, usr: str): + """ deletes given resource""" raise NotImplementedError() def get_owner(self, n_type: LocationDataType, oid: str): - # return the owner of the requested object; if multiple owners are set, - # return them is a list + """ + return the owner of the requested object; if multiple owners are set, + return them is a list + """ raise NotImplementedError() def check_perm(self, n_type: LocationDataType, oid: str, usr: str): - # check if the given user has permission to change the given object + """ check if the given user has permission to change the given object""" raise NotImplementedError() def add_perm(self, n_type: LocationDataType, oid: str, usr: str): - # add user to file perm + """add user to file perm""" raise NotImplementedError() def rm_perm(self, n_type: LocationDataType, oid: str, usr: str): - # remove user from file perm + """remove user from file perm""" raise NotImplementedError()