diff --git a/app.py b/app.py index 5df9c3c..b920842 100644 --- a/app.py +++ b/app.py @@ -16,13 +16,11 @@ from models import ( AuthModel, ColumnConditionCompat, CreateUserDefinition, - ItemsFieldSelectorList, TableDefinition, UserUpdateDefinition, OkResponse, ErrorResponse, AccessTokenResponse, - TableItemsResponse, CreateAssetResponse, ) from utils import ( @@ -74,7 +72,7 @@ async def getAccessToken(userData: AuthModel): user = check_user(connector, userData.username, userData.password) if not user: return JSONResponse( - ErrorResponse(error="Wrong username or password"), + ErrorResponse(error="Wrong username or password").dict(), status_code=status.HTTP_401_UNAUTHORIZED, ) @@ -92,14 +90,12 @@ async def getAccessToken(userData: AuthModel): }, }, ) -async def listTables( - response: Response, - access_token: str | None = Header(default=None), -): +async def listTables(access_token: str | None = Header(default=None)): is_admin = check_if_admin_access_token(connector, access_token) if not is_admin: return JSONResponse( - ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN + ErrorResponse(error="Not allowed").dict(), + status_code=status.HTTP_403_FORBIDDEN, ) return [TableDefinition.parse_obj(table) for table in connector.tables()] @@ -135,12 +131,14 @@ async def createTable( `column_name:column_type[:column_options]` Where *column_type* should be one of the following: - - serial + - serial:primary - str - bool - datetime - float - int + - int-asset + - int-user Also *column_options* can be one of the following: - unique @@ -154,33 +152,40 @@ async def createTable( "description:str", "is_active:bool", "price:float", - "quantity:int" + "quantity:int", + "creator_id:int-user", + "asset_id:int-asset" ] ``` Notes: - - you cannot use *unique* and *default* at the same time - - in current implementation you cannot use *default*, because there is no way to + 1. you cannot use *unique* and *default* at the same time + 2. in current implementation you cannot use *default*, because there is no way to specify default value """ is_admin = check_if_admin_access_token(connector, access_token) if not is_admin: return JSONResponse( - ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN + ErrorResponse(error="Not allowed").dict(), + status_code=status.HTTP_403_FORBIDDEN, ) try: columnsDefinition = parse_columns_from_definition(",".join(columns)) - create_table(connector, tableName, columnsDefinition) + ok, e = create_table(connector, tableName, columnsDefinition) + if not ok: + if e: + raise e + raise Exception("Unknown error") except psycopg.errors.UniqueViolation: return JSONResponse( - ErrorResponse(error="Table already exists"), + ErrorResponse(error="Table already exists").dict(), status_code=status.HTTP_409_CONFLICT, ) except Exception as e: return JSONResponse( - ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST + ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST ) return OkResponse() @@ -208,7 +213,8 @@ async def dropTable( is_admin = check_if_admin_access_token(connector, access_token) if not is_admin: return JSONResponse( - ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN + ErrorResponse(error="Not allowed").dict(), + status_code=status.HTTP_403_FORBIDDEN, ) try: @@ -219,14 +225,14 @@ async def dropTable( raise Exception("Unknown error") except Exception as e: return JSONResponse( - ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST + ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST ) return OkResponse() @app.post( - "/api/createUser", + "/api/users/+", name="Create user", responses={ 200: {"model": OkResponse, "description": "Table dropped successfully"}, @@ -251,7 +257,8 @@ async def createUser( is_admin = check_if_admin_access_token(connector, access_token) if not is_admin: return JSONResponse( - ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN + ErrorResponse(error="Not allowed").dict(), + status_code=status.HTTP_403_FORBIDDEN, ) try: @@ -262,19 +269,19 @@ async def createUser( raise Exception("Unknown error") except psycopg.errors.UniqueViolation: return JSONResponse( - ErrorResponse(error="Username already exists"), + ErrorResponse(error="Username already exists").dict(), status_code=status.HTTP_409_CONFLICT, ) except Exception as e: return JSONResponse( - ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST + ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST ) return OkResponse() @app.post( - "/api/updateUser", + "/api/users/*", name="Update user", responses={ 200: {"model": OkResponse, "description": "Table dropped successfully"}, @@ -295,7 +302,8 @@ async def updateUser( is_admin = check_if_admin_access_token(connector, access_token) if not is_admin: return JSONResponse( - ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN + ErrorResponse(error="Not allowed").dict(), + status_code=status.HTTP_403_FORBIDDEN, ) try: @@ -306,7 +314,62 @@ async def updateUser( raise Exception("Unknown error") except Exception as e: return JSONResponse( - ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST + ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST + ) + + return OkResponse() + + +@app.post( + "/api/users/{user_id}/-", + name="Remove user", + responses={ + 200: {"model": OkResponse, "description": "Table dropped successfully"}, + 400: { + "model": ErrorResponse, + "description": "Some generic error happened during user creation", + }, + 403: { + "model": ErrorResponse, + "description": "Requesting this endpoint requires admin-level user access token", + }, + 409: { + "model": ErrorResponse, + "description": "User with this username already exists", + }, + }, +) +async def removeUser( + user_id: int, + access_token: str | None = Header(default=None), +): + is_admin = check_if_admin_access_token(connector, access_token) + if not is_admin: + return JSONResponse( + ErrorResponse(error="Not allowed").dict(), + status_code=status.HTTP_403_FORBIDDEN, + ) + + try: + user = get_user_by_id(connector, user_id) + if not user: + raise Exception("User not found") + elif user.access_token == access_token: + raise Exception("Cannot remove yourself") + + ok, e = delete_user(connector, user_id) + if not ok: + if e: + raise e + raise Exception("Unknown error") + except psycopg.errors.UniqueViolation: + return JSONResponse( + ErrorResponse(error="Username already exists").dict(), + status_code=status.HTTP_409_CONFLICT, + ) + except Exception as e: + return JSONResponse( + ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST ) return OkResponse() @@ -316,7 +379,7 @@ async def updateUser( "/items/{tableName}", name="Get items from table", responses={ - 200: {"model": TableItemsResponse, "description": "Table items"}, + 200: {"model": list[dict[str, Any]], "description": "Table items"}, 400: { "model": ErrorResponse, "description": "Some generic error happened during getting table items", @@ -333,27 +396,31 @@ async def updateUser( ) async def items( tableName: str, - selector: ItemsFieldSelectorList, + fields: list[str] = ["*"], + where: list[ColumnConditionCompat] = [], access_token: str | None = Header(default=None), ): table_info = connector.getTable(tableName) if not table_info: return JSONResponse( - ErrorResponse(error="Table not found"), + ErrorResponse(error="Table not found").dict(), status_code=status.HTTP_404_NOT_FOUND, ) is_admin = check_if_admin_access_token(connector, access_token) if table_info["system"] and not is_admin: return JSONResponse( - ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN + ErrorResponse(error="Not allowed").dict(), + status_code=status.HTTP_403_FORBIDDEN, ) columns = parse_columns_from_definition(table_info["columns"]) columnsNames = set(column.name for column in columns) - userSelectedColumns = list(set(selector.fields)) if selector.fields else ["*"] - if userSelectedColumns != ["*"]: - for column in userSelectedColumns: + + if fields == ["*"]: + fields = list(columnsNames) + else: + for column in fields: if column not in columnsNames: return JSONResponse( ErrorResponse( @@ -361,39 +428,54 @@ async def items( ), status_code=status.HTTP_404_NOT_FOUND, ) - else: - userSelectedColumns = list(columnsNames) - user, group = get_user_by_access_token(connector, access_token) - if not user: - return JSONResponse( - ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN - ) + if where: + for key in where: + if key.column not in columnsNames: + return JSONResponse( + ErrorResponse(error=f"Column {key} not found on table {tableName}"), + status_code=status.HTTP_404_NOT_FOUND, + ) + + _, group = get_user_by_access_token(connector, access_token) if not is_admin: allowedColumns = get_allowed_columns_for_group( - connector, tableName, group.id if group else -1 + connector, tableName, group.id if group else 1 # 1 is anonymous group ) if not allowedColumns: return JSONResponse( - ErrorResponse(error="Not allowed"), + ErrorResponse(error="Not allowed").dict(), status_code=status.HTTP_403_FORBIDDEN, ) elif len(allowedColumns) == 1 and allowedColumns[0] == "*": pass else: - for column in userSelectedColumns: + for column in fields: if column not in allowedColumns: return JSONResponse( - ErrorResponse(error="Not allowed"), + ErrorResponse(error="Not allowed").dict(), + status_code=status.HTTP_403_FORBIDDEN, + ) + for column in where: + if column.column not in allowedColumns: + return JSONResponse( + ErrorResponse(error="Not allowed").dict(), status_code=status.HTTP_403_FORBIDDEN, ) - table_items = connector.selectFromTable( - tableName, selector.fields if selector.fields else ["*"] - ) + try: + table_items = connector.selectFromTable( + tableName, + fields, + [ColumnCondition(w.column, w.operator, w.value) for w in where], + ) + except Exception as e: + return JSONResponse( + ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST + ) - return TableItemsResponse(items=table_items) + return table_items @app.post( @@ -427,14 +509,15 @@ async def itemsCreate( table_info = connector.getTable(tableName) if not table_info: return JSONResponse( - ErrorResponse(error="Table not found"), + ErrorResponse(error="Table not found").dict(), status_code=status.HTTP_404_NOT_FOUND, ) is_admin = check_if_admin_access_token(connector, access_token) if table_info["system"] and not is_admin: return JSONResponse( - ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN + ErrorResponse(error="Not allowed").dict(), + status_code=status.HTTP_403_FORBIDDEN, ) user, group = get_user_by_access_token(connector, access_token) @@ -444,7 +527,7 @@ async def itemsCreate( ) if not allowedColumns: return JSONResponse( - ErrorResponse(error="Not allowed"), + ErrorResponse(error="Not allowed").dict(), status_code=status.HTTP_403_FORBIDDEN, ) elif len(allowedColumns) == 1 and allowedColumns[0] == "*": @@ -453,7 +536,7 @@ async def itemsCreate( for column in item: if column not in allowedColumns: return JSONResponse( - ErrorResponse(error="Not allowed"), + ErrorResponse(error="Not allowed").dict(), status_code=status.HTTP_403_FORBIDDEN, ) @@ -461,17 +544,17 @@ async def itemsCreate( connector.insertIntoTable(tableName, item) except psycopg.errors.UndefinedColumn: return JSONResponse( - ErrorResponse(error="Column not found"), + ErrorResponse(error="Column not found").dict(), status_code=status.HTTP_404_NOT_FOUND, ) except psycopg.errors.UniqueViolation: return JSONResponse( - ErrorResponse(error="Unique violation"), + ErrorResponse(error="Unique violation").dict(), status_code=status.HTTP_409_CONFLICT, ) except Exception as e: return JSONResponse( - ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST + ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST ) return OkResponse() @@ -509,14 +592,14 @@ async def itemsUpdate( table_info = connector.getTable(tableName) if not table_info: return JSONResponse( - ErrorResponse(error="Table not found"), + ErrorResponse(error="Table not found").dict(), status_code=status.HTTP_404_NOT_FOUND, ) is_admin = check_if_admin_access_token(connector, access_token) if table_info["system"] and not is_admin: return JSONResponse( - ErrorResponse(error="Not allowed"), + ErrorResponse(error="Not allowed").dict(), status_code=status.HTTP_403_FORBIDDEN, ) @@ -527,7 +610,7 @@ async def itemsUpdate( ) if not allowedColumns: return JSONResponse( - ErrorResponse(error="Not allowed"), + ErrorResponse(error="Not allowed").dict(), status_code=status.HTTP_403_FORBIDDEN, ) elif len(allowedColumns) == 1 and allowedColumns[0] == "*": @@ -536,7 +619,7 @@ async def itemsUpdate( for column in item: if column not in allowedColumns: return JSONResponse( - ErrorResponse(error="Not allowed"), + ErrorResponse(error="Not allowed").dict(), status_code=status.HTTP_403_FORBIDDEN, ) @@ -551,17 +634,17 @@ async def itemsUpdate( ) except psycopg.errors.UniqueViolation: return JSONResponse( - ErrorResponse(error="Unique violation"), + ErrorResponse(error="Unique violation").dict(), status_code=status.HTTP_409_CONFLICT, ) except psycopg.errors.UndefinedColumn: return JSONResponse( - ErrorResponse(error="Column not found"), + ErrorResponse(error="Column not found").dict(), status_code=status.HTTP_404_NOT_FOUND, ) except Exception as e: return JSONResponse( - ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST + ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST ) return OkResponse() @@ -594,14 +677,14 @@ async def itemsDelete( table_info = connector.getTable(tableName) if not table_info: return JSONResponse( - ErrorResponse(error="Table not found"), + ErrorResponse(error="Table not found").dict(), status_code=status.HTTP_404_NOT_FOUND, ) is_admin = check_if_admin_access_token(connector, access_token) if table_info["system"] and not is_admin: return JSONResponse( - ErrorResponse(error="Not allowed"), + ErrorResponse(error="Not allowed").dict(), status_code=status.HTTP_403_FORBIDDEN, ) @@ -612,14 +695,14 @@ async def itemsDelete( ) if not allowedColumns: return JSONResponse( - ErrorResponse(error="Not allowed"), + ErrorResponse(error="Not allowed").dict(), status_code=status.HTTP_403_FORBIDDEN, ) elif len(allowedColumns) == 1 and allowedColumns[0] == "*": pass else: return JSONResponse( - ErrorResponse(error="Not allowed"), + ErrorResponse(error="Not allowed").dict(), status_code=status.HTTP_403_FORBIDDEN, ) @@ -630,7 +713,7 @@ async def itemsDelete( ) except Exception as e: return JSONResponse( - ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST + ErrorResponse(error=str(e)).dict(), status_code=status.HTTP_400_BAD_REQUEST ) return OkResponse() @@ -649,8 +732,8 @@ async def itemsDelete( }, }, ) -async def getAsset(fid: str, access_token: str | None = Header(default=None)): - asset = get_asset(connector, access_token, fid) +async def getAsset(fid: str): + asset = get_asset(connector, fid) if not asset: return status.HTTP_404_NOT_FOUND @@ -700,7 +783,7 @@ async def createAsset( user, _ = get_user_by_access_token(connector, access_token) if not user: return JSONResponse( - ErrorResponse(error="Not allowed"), + ErrorResponse(error="Not allowed").dict(), status_code=status.HTTP_403_FORBIDDEN, ) @@ -720,18 +803,139 @@ async def createAsset( ) if not result: return JSONResponse( - ErrorResponse(error="Failed put asset into storage"), + ErrorResponse(error="Failed put asset into storage").dict(), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - if not create_asset(connector, filename, "", str(result.version_id)): + ok, e = create_asset(connector, filename, "", str(result.version_id)) + if not ok: + if e: + return JSONResponse( + ErrorResponse(error=str(e)).dict(), + status_code=status.HTTP_400_BAD_REQUEST, + ) return JSONResponse( - ErrorResponse(error="Failed to create asset"), + ErrorResponse(error="Failed to create asset").dict(), status_code=status.HTTP_400_BAD_REQUEST, ) return CreateAssetResponse(fid=result.version_id) +@app.post( + "/assets/{asset_id}/*", + name="Update asset description", + responses={ + 200: { + "model": OkResponse, + "description": "Asset description updated successfully", + }, + 400: { + "model": ErrorResponse, + "description": "Some generic error happened during updating asset", + }, + 403: { + "model": ErrorResponse, + "description": "Requesting this endpoint requires user access token", + }, + 404: { + "model": ErrorResponse, + "description": "Asset not found", + }, + }, +) +async def updateAsset( + asset_id: int, + asset_description: str, + access_token: str | None = Header(default=None), +): + user = get_user_by_access_token(connector, access_token) + if not user: + return JSONResponse( + ErrorResponse(error="Not allowed").dict(), + status_code=status.HTTP_403_FORBIDDEN, + ) + + ok, e = update_asset(connector, asset_id, asset_description) + if not ok: + if e: + return JSONResponse( + ErrorResponse(error=str(e)).dict(), + status_code=status.HTTP_400_BAD_REQUEST, + ) + return JSONResponse( + ErrorResponse(error="Asset not found").dict(), + status_code=status.HTTP_404_NOT_FOUND, + ) + + return OkResponse() + + +@app.post( + "/assets/{asset_id}/-", + name="Remove asset", + responses={ + 200: { + "model": OkResponse, + "description": "Asset removed successfully", + }, + 400: { + "model": ErrorResponse, + "description": "Something went wrong during removing asset", + }, + 403: { + "model": ErrorResponse, + "description": "Requesting this endpoint requires user access token", + }, + 404: { + "model": ErrorResponse, + "description": "Asset not found", + }, + }, +) +async def removeAsset( + asset_id: int, + check_references: bool = True, + delete_referencing: bool = False, + access_token: str | None = Header(default=None), +): + user = get_user_by_access_token(connector, access_token) + if not user: + return JSONResponse( + ErrorResponse(error="Not allowed").dict(), + status_code=status.HTTP_403_FORBIDDEN, + ) + + asset = get_asset_by_id(connector, asset_id) + if not asset: + return JSONResponse( + ErrorResponse(error="Asset not found").dict(), + status_code=status.HTTP_404_NOT_FOUND, + ) + + try: + minioClient.remove_object(BUCKET_NAME, asset.fid) + except Exception as e: + logger.error(f"Failed to remove asset from storage: {e}") + return JSONResponse( + ErrorResponse(error=str(e)).dict(), + status_code=status.HTTP_400_BAD_REQUEST, + ) + + ok, e = remove_asset(connector, asset_id, check_references, delete_referencing) + if not ok: + if e: + return JSONResponse( + ErrorResponse(error=str(e)).dict(), + status_code=status.HTTP_400_BAD_REQUEST, + ) + return JSONResponse( + ErrorResponse(error="Unknown error").dict(), + status_code=status.HTTP_400_BAD_REQUEST, + ) + + return OkResponse() + + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/db_addendum.py b/db_addendum.py index 8b002fe..27c58e6 100644 --- a/db_addendum.py +++ b/db_addendum.py @@ -1,4 +1,4 @@ -from psycopg.sql import SQL, Identifier, Literal, Composed +from psycopg.sql import SQL, Identifier from based.columns import ( IntegerColumnDefinition, ) @@ -12,7 +12,7 @@ class UserRefColumnDefinition(IntegerColumnDefinition): return SQL("{} INTEGER NOT NULL").format(Identifier(self.name)) def serialize(self): - return f"{self.name}:asset" + return f"{self.name}:int-user" class AssetRefColumnDefinition(IntegerColumnDefinition): @@ -23,4 +23,4 @@ class AssetRefColumnDefinition(IntegerColumnDefinition): return SQL("{} INTEGER NOT NULL").format(Identifier(self.name)) def serialize(self): - return f"{self.name}:asset" + return f"{self.name}:int-asset" diff --git a/db_models.py b/db_models.py index e56ade1..2f6de26 100644 --- a/db_models.py +++ b/db_models.py @@ -6,6 +6,8 @@ from based.columns import ( ) from pydantic import BaseModel +from db_addendum import UserRefColumnDefinition + class AccessType(enum.Enum): READ = "read" @@ -19,7 +21,6 @@ META_INFO_TABLE_SCHEMA = [ PrimarySerialColumnDefinition("id"), TextColumnDefinition("name", unique=True), TextColumnDefinition("value"), - TextColumnDefinition("allowed_columns", default="*"), ] @@ -27,7 +28,6 @@ class MetaInfo(BaseModel): id: int name: str value: str - allowed_columns: str USER_GROUP_TABLE_NAME = "user_group" @@ -73,7 +73,7 @@ class User(BaseModel): USER_IN_USER_GROUP_JOIN_TABLE_NAME = "user_in_user_group" USER_IN_USER_GROUP_JOIN_TABLE_SCHEMA = [ PrimarySerialColumnDefinition("id"), - IntegerColumnDefinition("user_id"), + UserRefColumnDefinition("user_id"), IntegerColumnDefinition("user_group_id"), ] @@ -108,7 +108,7 @@ ASSETS_TABLE_SCHEMA = [ TextColumnDefinition("name"), TextColumnDefinition("description", default=""), TextColumnDefinition("fid"), - TextColumnDefinition("catalog", default="/root"), + TextColumnDefinition("tags", default=""), ] @@ -118,19 +118,3 @@ class Asset(BaseModel): description: str fid: str catalog: str - - -ASSET_ACCESS_TABLE_NAME = "asset_access" -ASSET_ACCESS_TABLE_SCHEMA = [ - PrimarySerialColumnDefinition("id"), - IntegerColumnDefinition("user_group_id"), - IntegerColumnDefinition("asset_id"), - TextColumnDefinition("access_type"), -] - - -class AssetAccess(BaseModel): - id: int - user_group_id: int - asset_id: int - access_type: str diff --git a/dba.py b/dba.py index f96382b..bb228bc 100644 --- a/dba.py +++ b/dba.py @@ -1,8 +1,11 @@ import logging from secrets import token_hex from based.db import DBConnector, ColumnCondition, ColumnUpdate, ColumnDefinition +from db_addendum import AssetRefColumnDefinition, UserRefColumnDefinition from db_models import * +from models import TableDefinition from secutils import hash_password +import utils logger = logging.getLogger(__name__) @@ -46,25 +49,18 @@ def bootstrapDB(conn: DBConnector): system=True, ) - if not conn.tableExists(ASSET_ACCESS_TABLE_NAME): - logger.info("Creating asset access table") - conn.createTable( - ASSET_ACCESS_TABLE_NAME, - ASSET_ACCESS_TABLE_SCHEMA, - system=True, - ) - meta = get_metadata(conn, "admin_created") testAdminCreated = meta and meta.value == "yes" if not testAdminCreated: logger.info("Creating admin user and group") create_user(conn, "admin", "admin") - create_group(conn, "admin") + create_group(conn, "anonymous", "Default group for anonymous access") + create_group(conn, "admin", "Administrator group") users = list_users(conn) groups = list_groups(conn) - set_user_group(conn, users[0].id, groups[0].id) + set_user_group(conn, users[0].id, groups[1].id) add_metadata(conn, "admin_created", "yes") @@ -123,6 +119,45 @@ def update_user(conn: DBConnector, id: int, password: str, access_token: str): return False, e +def delete_user( + conn: DBConnector, + id: int, + check_references: bool = True, + delete_referencing: bool = False, +): + try: + if check_references: + table_with_user_ref: list[tuple[str, ColumnDefinition]] = [] + for table_def in conn.tables(): + table = TableDefinition.parse_obj(table_def) + columns = utils.parse_columns_from_definition(table.columns) + for column in columns: + if column is UserRefColumnDefinition: + table_with_user_ref.append((table.table_name, column)) + + if delete_referencing: + for table_name, column in table_with_user_ref: + conn.deleteFromTable( + table_name, + [ + ColumnCondition(column.name, "eq", id), + ], + ) + elif table_with_user_ref: + raise Exception("User is referenced in other tables") + + conn.deleteFromTable( + USERS_TABLE_NAME, + [ + ColumnCondition("id", "eq", id), + ], + ) + return True, None + except Exception as e: + logger.exception(e) + return False, e + + def get_user_by_username(conn: DBConnector, username: str): try: users = conn.filterFromTable( @@ -270,8 +305,8 @@ def get_user_group(conn: DBConnector, user_id: int): logger.warning(f"User with id {user_id} not found, so no group") return None - uiug = UserInUserGroup.parse_obj(grp_usr_joint[0]) - return get_group_by_id(conn, uiug.user_group_id) + u_i_u_g = UserInUserGroup.parse_obj(grp_usr_joint[0]) + return get_group_by_id(conn, u_i_u_g.user_group_id) except Exception as e: logger.exception(e) return None @@ -299,7 +334,7 @@ def list_users(conn: DBConnector) -> list[User]: return [] -def list_groups(conn: DBConnector): +def list_groups(conn: DBConnector) -> list[UserGroup]: try: groups = conn.selectFromTable(USER_GROUP_TABLE_NAME, ["*"]) return [*map(UserGroup.parse_obj, groups)] @@ -311,10 +346,10 @@ def list_groups(conn: DBConnector): def create_table(conn: DBConnector, table_name: str, schema: list[ColumnDefinition]): try: conn.createTable(table_name, schema) - return True + return True, None except Exception as e: logger.exception(e) - return False + return False, e def get_table_access_level( @@ -409,37 +444,68 @@ def create_asset(conn: DBConnector, name: str, description: str, fid: str): "fid": fid, }, ) - # TODO: add asset access - # TODO: add asset to minio - return True + return True, None except Exception as e: logger.exception(e) - return None + return False, None -def remove_asset(conn: DBConnector, token: str | None, asset_id: int): +def update_asset(conn: DBConnector, asset_id: int, asset_description: str): try: + conn.updateDataInTable( + ASSETS_TABLE_NAME, + [ColumnUpdate("description", asset_description)], + [ColumnCondition("id", "eq", asset_id)], + ) + return True, None + except Exception as e: + logger.exception(e) + return False, e + + +def remove_asset( + conn: DBConnector, + asset_id: int, + check_references: bool = True, + delete_referencing: bool = False, +): + try: + if check_references: + table_with_asset_ref: list[tuple[str, ColumnDefinition]] = [] + for table_def in conn.tables(): + table = TableDefinition.parse_obj(table_def) + columns = utils.parse_columns_from_definition(table.columns) + for column in columns: + if column is AssetRefColumnDefinition: + table_with_asset_ref.append((table.table_name, column)) + + if delete_referencing: + for table_name, column in table_with_asset_ref: + conn.deleteFromTable( + table_name, + [ + ColumnCondition(column.name, "eq", asset_id), + ], + ) + elif table_with_asset_ref: + raise Exception("Asset is referenced in other tables") + conn.deleteFromTable(ASSETS_TABLE_NAME, [ColumnCondition("id", "eq", asset_id)]) - # TODO: remove asset access - # TODO: remove asset from minio - return True + return True, None except Exception as e: logger.exception(e) - return False + return False, e -def get_asset(conn: DBConnector, token: str | None, fid: str): +def get_asset(conn: DBConnector, fid: str): try: - user, group = get_user_by_access_token(conn, token) assets = conn.filterFromTable( ASSETS_TABLE_NAME, ["*"], [ColumnCondition("fid", "eq", fid)] ) - print(assets) + if len(assets) == 0: return None asset = Asset.parse_obj(assets[0]) - asset_access = get_asset_access(conn, asset.id) - # TODO: check if user has access to asset return asset except Exception as e: @@ -447,44 +513,17 @@ def get_asset(conn: DBConnector, token: str | None, fid: str): return None -def create_asset_access(conn: DBConnector, asset_id: int, user_group_id: int): +def get_asset_by_id(conn: DBConnector, asset_id: int): try: - conn.insertIntoTable( - ASSET_ACCESS_TABLE_NAME, - {"asset_id": asset_id, "user_group_id": user_group_id}, + assets = conn.filterFromTable( + ASSETS_TABLE_NAME, ["*"], [ColumnCondition("id", "eq", asset_id)] ) - return True - except Exception as e: - # NOTE: this should not happen ever - logger.exception(e) - return False + if len(assets) == 0: + return None + asset = Asset.parse_obj(assets[0]) -def get_asset_access(conn: DBConnector, asset_id: int): - try: - access = conn.filterFromTable( - ASSET_ACCESS_TABLE_NAME, - ["*"], - [ColumnCondition("asset_id", "eq", asset_id)], - ) - if not access: - return AccessType.NONE - access = AssetAccess.parse_obj(access[0]) - if access.access_type == "r": - return AccessType.READ - elif access.access_type == "w": - return AccessType.WRITE - elif access.access_type == "rw": - return AccessType.READ_WRITE - else: - return AccessType.NONE + return asset except Exception as e: logger.exception(e) - return AccessType.NONE - - -def change_asset_access( - conn: DBConnector, asset_id: int, user_group_id: int, access_type: AccessType -): - # TODO: implement - raise NotImplementedError() + return None diff --git a/models.py b/models.py index f31b05d..126a02a 100644 --- a/models.py +++ b/models.py @@ -8,10 +8,6 @@ class AuthModel(BaseModel): password: str -class ItemsFieldSelectorList(BaseModel): - fields: list[str] | None = [] - - class CreateUserDefinition(BaseModel): username: str password: str @@ -56,10 +52,6 @@ class AccessTokenResponse(BaseModel): access_token: str -class TableItemsResponse(BaseModel): - items: list[dict[str, Any]] - - class CreateAssetResponse(BaseModel): ok: bool = True fid: str diff --git a/utils.py b/utils.py index 105d000..a28fc9b 100644 --- a/utils.py +++ b/utils.py @@ -8,7 +8,8 @@ from based.columns import ( DoubleColumnDefinition, IntegerColumnDefinition, ) -from dba import get_user_by_access_token +from db_addendum import AssetRefColumnDefinition, UserRefColumnDefinition +import dba def check_if_admin_access_token( @@ -17,7 +18,7 @@ def check_if_admin_access_token( if access_token is None: return False - user, group = get_user_by_access_token(connector, access_token) + user, group = dba.get_user_by_access_token(connector, access_token) if user is None or group is None or group.name != "admin": return False @@ -64,6 +65,12 @@ def get_column_from_definition(definition: str) -> ColumnDefinition | None: td.has_default = has_default return td + case [name, "int-user", *rest]: + return UserRefColumnDefinition(name) + + case [name, "int-asset", *rest]: + return AssetRefColumnDefinition(name) + return None