Improvements upon front lead to improvements here
This commit is contained in:
parent
3414b5c334
commit
34ae028698
6 changed files with 395 additions and 169 deletions
348
app.py
348
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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
24
db_models.py
24
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
|
||||
|
|
|
|||
167
dba.py
167
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
11
utils.py
11
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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue