laia-gen-lib 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- laia-gen-lib-0.1.0/PKG-INFO +5 -0
- laia-gen-lib-0.1.0/laia_gen_lib.egg-info/PKG-INFO +5 -0
- laia-gen-lib-0.1.0/laia_gen_lib.egg-info/SOURCES.txt +27 -0
- laia-gen-lib-0.1.0/laia_gen_lib.egg-info/dependency_links.txt +1 -0
- laia-gen-lib-0.1.0/laia_gen_lib.egg-info/requires.txt +3 -0
- laia-gen-lib-0.1.0/laia_gen_lib.egg-info/top_level.txt +2 -0
- laia-gen-lib-0.1.0/laiagenlib/__init__.py +3 -0
- laia-gen-lib-0.1.0/laiagenlib/crud/__init__.py +2 -0
- laia-gen-lib-0.1.0/laiagenlib/crud/crud.py +24 -0
- laia-gen-lib-0.1.0/laiagenlib/crud/crud_mongo_impl.py +69 -0
- laia-gen-lib-0.1.0/laiagenlib/crud/schemas.py +13 -0
- laia-gen-lib-0.1.0/laiagenlib/main.py +33 -0
- laia-gen-lib-0.1.0/laiagenlib/models/AccessRights.py +73 -0
- laia-gen-lib-0.1.0/laiagenlib/models/Model.py +165 -0
- laia-gen-lib-0.1.0/laiagenlib/models/Openapi.py +128 -0
- laia-gen-lib-0.1.0/laiagenlib/models/Role.py +27 -0
- laia-gen-lib-0.1.0/laiagenlib/models/Route.py +10 -0
- laia-gen-lib-0.1.0/laiagenlib/models/__init__.py +5 -0
- laia-gen-lib-0.1.0/laiagenlib/utils/__init__.py +2 -0
- laia-gen-lib-0.1.0/laiagenlib/utils/logger.py +34 -0
- laia-gen-lib-0.1.0/laiagenlib/utils/utils.py +41 -0
- laia-gen-lib-0.1.0/setup.cfg +4 -0
- laia-gen-lib-0.1.0/setup.py +13 -0
- laia-gen-lib-0.1.0/tests/__init__.py +0 -0
- laia-gen-lib-0.1.0/tests/models/__init__.py +0 -0
- laia-gen-lib-0.1.0/tests/models/test_access_rights.py +155 -0
- laia-gen-lib-0.1.0/tests/models/test_model.py +258 -0
- laia-gen-lib-0.1.0/tests/models/test_openapi.py +26 -0
- laia-gen-lib-0.1.0/tests/models/test_role.py +58 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
setup.py
|
|
2
|
+
laia_gen_lib.egg-info/PKG-INFO
|
|
3
|
+
laia_gen_lib.egg-info/SOURCES.txt
|
|
4
|
+
laia_gen_lib.egg-info/dependency_links.txt
|
|
5
|
+
laia_gen_lib.egg-info/requires.txt
|
|
6
|
+
laia_gen_lib.egg-info/top_level.txt
|
|
7
|
+
laiagenlib/__init__.py
|
|
8
|
+
laiagenlib/main.py
|
|
9
|
+
laiagenlib/crud/__init__.py
|
|
10
|
+
laiagenlib/crud/crud.py
|
|
11
|
+
laiagenlib/crud/crud_mongo_impl.py
|
|
12
|
+
laiagenlib/crud/schemas.py
|
|
13
|
+
laiagenlib/models/AccessRights.py
|
|
14
|
+
laiagenlib/models/Model.py
|
|
15
|
+
laiagenlib/models/Openapi.py
|
|
16
|
+
laiagenlib/models/Role.py
|
|
17
|
+
laiagenlib/models/Route.py
|
|
18
|
+
laiagenlib/models/__init__.py
|
|
19
|
+
laiagenlib/utils/__init__.py
|
|
20
|
+
laiagenlib/utils/logger.py
|
|
21
|
+
laiagenlib/utils/utils.py
|
|
22
|
+
tests/__init__.py
|
|
23
|
+
tests/models/__init__.py
|
|
24
|
+
tests/models/test_access_rights.py
|
|
25
|
+
tests/models/test_model.py
|
|
26
|
+
tests/models/test_openapi.py
|
|
27
|
+
tests/models/test_role.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import TypeVar, Optional, Dict
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
T = TypeVar('T', bound='BaseModel')
|
|
5
|
+
|
|
6
|
+
class CRUD:
|
|
7
|
+
|
|
8
|
+
def __init__(self, db: Dict[str, any]):
|
|
9
|
+
self.db = db
|
|
10
|
+
|
|
11
|
+
async def get_items(model_name: str, skip: int = 0, limit: int = 10, filters: Optional[dict] = None):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
async def get_item(model_name: str, item_id: str):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
async def post_item(model_name: str, item: T):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
async def put_item(model_name: str, item_id: str, update_fields: dict):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
async def delete_item(model_name: str, item_id: str):
|
|
24
|
+
pass
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import TypeVar, Optional, Dict
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from pymongo.collection import ReturnDocument
|
|
4
|
+
from .schemas import list_serial, individual_serial
|
|
5
|
+
from bson import ObjectId
|
|
6
|
+
from .crud import CRUD
|
|
7
|
+
|
|
8
|
+
T = TypeVar('T', bound='BaseModel')
|
|
9
|
+
|
|
10
|
+
class CRUDMongoImpl(CRUD):
|
|
11
|
+
|
|
12
|
+
def __init__(self, db: Dict[str, any]):
|
|
13
|
+
super().__init__(db)
|
|
14
|
+
|
|
15
|
+
async def get_items(self, model_name: str, skip: int = 0, limit: int = 10, filters: Optional[dict] = None, orders: Optional[dict] = None):
|
|
16
|
+
collection = self.db[model_name]
|
|
17
|
+
|
|
18
|
+
query = filters or {}
|
|
19
|
+
sorts = orders or {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
items = collection.find(query, skip=skip, limit=limit, sort=sorts)
|
|
23
|
+
serialized_items = list_serial(items)
|
|
24
|
+
|
|
25
|
+
total_count = collection.count_documents(query)
|
|
26
|
+
|
|
27
|
+
return serialized_items, total_count
|
|
28
|
+
|
|
29
|
+
async def get_item(self, model_name: str, item_id: str):
|
|
30
|
+
collection = self.db[model_name]
|
|
31
|
+
|
|
32
|
+
item = collection.find_one({'_id': ObjectId(item_id)})
|
|
33
|
+
|
|
34
|
+
if item:
|
|
35
|
+
return individual_serial(item)
|
|
36
|
+
raise ValueError(f"{model_name} with ID {item_id} not found")
|
|
37
|
+
|
|
38
|
+
async def post_item(self, model_name: str, item: T):
|
|
39
|
+
collection = self.db[model_name]
|
|
40
|
+
item_dict = dict(item)
|
|
41
|
+
item_dict.pop('id', None)
|
|
42
|
+
created_result = collection.insert_one(item_dict)
|
|
43
|
+
inserted_id = created_result.inserted_id
|
|
44
|
+
item_dict['id'] = str(inserted_id)
|
|
45
|
+
item_dict.pop('_id', None)
|
|
46
|
+
|
|
47
|
+
return item_dict
|
|
48
|
+
|
|
49
|
+
async def put_item(self, model_name: str, item_id: str, update_fields: dict):
|
|
50
|
+
collection = self.db[model_name]
|
|
51
|
+
update_query = {'$set': update_fields}
|
|
52
|
+
|
|
53
|
+
updated_item = collection.find_one_and_update(
|
|
54
|
+
{'_id': ObjectId(item_id)},
|
|
55
|
+
update_query,
|
|
56
|
+
return_document=ReturnDocument.AFTER,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if updated_item:
|
|
60
|
+
return individual_serial(updated_item)
|
|
61
|
+
raise Exception
|
|
62
|
+
|
|
63
|
+
async def delete_item(self, model_name: str, item_id: str):
|
|
64
|
+
collection = self.db[model_name]
|
|
65
|
+
deleted_item = collection.find_one_and_delete({'_id': ObjectId(item_id)})
|
|
66
|
+
if deleted_item:
|
|
67
|
+
return individual_serial(deleted_item)
|
|
68
|
+
raise Exception
|
|
69
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import List, TypeVar, Type
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
T = TypeVar('T', bound='BaseModel')
|
|
5
|
+
|
|
6
|
+
def individual_serial(item: T) -> dict:
|
|
7
|
+
item['_id'] = str(item['_id'])
|
|
8
|
+
item['id'] = str(item['_id'])
|
|
9
|
+
del item['_id']
|
|
10
|
+
return item
|
|
11
|
+
|
|
12
|
+
def list_serial(items: List[T]) -> list[dict]:
|
|
13
|
+
return[individual_serial(item) for item in items]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
3
|
+
from .crud.crud import CRUD
|
|
4
|
+
from .models.Openapi import OpenAPI
|
|
5
|
+
from .utils.utils import create_models_file
|
|
6
|
+
from .utils.logger import _logger
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
class LaiaFastApi():
|
|
10
|
+
|
|
11
|
+
def __init__(self, openapi, db, crud: CRUD):
|
|
12
|
+
self.db = db
|
|
13
|
+
self.crud_instance = crud(db)
|
|
14
|
+
self.openapi_path = openapi
|
|
15
|
+
self.openapi = OpenAPI(openapi)
|
|
16
|
+
self.api = FastAPI()
|
|
17
|
+
self.api.add_middleware(
|
|
18
|
+
CORSMiddleware,
|
|
19
|
+
allow_origins=["*"],
|
|
20
|
+
allow_credentials=True,
|
|
21
|
+
allow_methods=["*"],
|
|
22
|
+
allow_headers=["*"],
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
models_dir = os.path.join(os.path.dirname(self.openapi_path), "models")
|
|
26
|
+
if not os.path.exists(models_dir):
|
|
27
|
+
os.makedirs(models_dir)
|
|
28
|
+
|
|
29
|
+
models_path = os.path.join(models_dir, "models.py")
|
|
30
|
+
create_models_file(self.openapi_path, models_path)
|
|
31
|
+
self.openapi.create_crud_routes(self.api, self.crud_instance, models_path)
|
|
32
|
+
|
|
33
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import Dict, Type
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from ..crud.crud import CRUD
|
|
4
|
+
from ..utils.logger import _logger
|
|
5
|
+
from ..utils.utils import create_element
|
|
6
|
+
|
|
7
|
+
class AccessRights(BaseModel):
|
|
8
|
+
role: str
|
|
9
|
+
model: str
|
|
10
|
+
operations: Dict[str, int] = {}
|
|
11
|
+
fields_create: Dict[str, int] = {}
|
|
12
|
+
fields_edit: Dict[str, int] = {}
|
|
13
|
+
fields_visible: Dict[str, int] = {}
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
async def create(cls, new_access_rights: dict, model: Type, user_roles: list, crud_instance: CRUD):
|
|
17
|
+
_logger.info(f"Creating new AccessRights with values: {new_access_rights}")
|
|
18
|
+
|
|
19
|
+
if 'role' in new_access_rights and 'model' in new_access_rights:
|
|
20
|
+
pass
|
|
21
|
+
else:
|
|
22
|
+
raise ValueError("Missing required parameters")
|
|
23
|
+
|
|
24
|
+
if new_access_rights.get("model") != model.__name__.lower():
|
|
25
|
+
raise ValueError("Provided model name does not match the class model name")
|
|
26
|
+
|
|
27
|
+
if "admin" not in user_roles:
|
|
28
|
+
raise PermissionError("Only users with 'admin' role can create access rights")
|
|
29
|
+
|
|
30
|
+
operations = new_access_rights.get("operations", {})
|
|
31
|
+
valid_operations = {"create", "read", "update", "delete", "search"}
|
|
32
|
+
|
|
33
|
+
for operation in operations:
|
|
34
|
+
if operation not in valid_operations or not isinstance(operations[operation], int):
|
|
35
|
+
raise ValueError(f"Invalid format for operation {operation}")
|
|
36
|
+
|
|
37
|
+
fields_to_check = ["fields_create", "fields_edit", "fields_visible"]
|
|
38
|
+
|
|
39
|
+
for field_type in fields_to_check:
|
|
40
|
+
fields = new_access_rights.get(field_type, {})
|
|
41
|
+
if not isinstance(fields, dict):
|
|
42
|
+
raise ValueError(f"Invalid format for {field_type}")
|
|
43
|
+
|
|
44
|
+
model_fields = []
|
|
45
|
+
for class_in_hierarchy in model.mro():
|
|
46
|
+
if hasattr(class_in_hierarchy, '__annotations__'):
|
|
47
|
+
model_fields.extend([field for field in class_in_hierarchy.__annotations__ if not field.startswith("_")])
|
|
48
|
+
|
|
49
|
+
for field_name, field_value in fields.items():
|
|
50
|
+
if field_name not in model_fields or not isinstance(field_value, int):
|
|
51
|
+
raise ValueError(f"Invalid field {field_name} for {field_type}")
|
|
52
|
+
|
|
53
|
+
existing_access_rights, _ = await crud_instance.get_items(
|
|
54
|
+
"accessrights",
|
|
55
|
+
skip=0,
|
|
56
|
+
limit=10,
|
|
57
|
+
filters={
|
|
58
|
+
"model": new_access_rights.get("model"),
|
|
59
|
+
"role": new_access_rights.get("role")
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if existing_access_rights:
|
|
64
|
+
raise ValueError("AccessRights with the same role and model already exists")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
access_rights = AccessRights(**new_access_rights)
|
|
68
|
+
except Exception:
|
|
69
|
+
raise ValueError("Missing required parameters")
|
|
70
|
+
|
|
71
|
+
created_accessrights = await create_element(access_rights, crud_instance)
|
|
72
|
+
_logger.info("AccessRights created successfully")
|
|
73
|
+
return cls(**created_accessrights)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from typing import Type, List, Dict, Any
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from ..utils.utils import create_element
|
|
4
|
+
from ..crud.crud import CRUD
|
|
5
|
+
from .AccessRights import AccessRights
|
|
6
|
+
from ..utils.logger import _logger
|
|
7
|
+
|
|
8
|
+
class LaiaBaseModel(BaseModel):
|
|
9
|
+
id: str = ""
|
|
10
|
+
name: str
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
async def create(cls, new_element: dict, model: Type, user_roles: list, crud_instance: CRUD):
|
|
14
|
+
_logger.info(f"Creating new {model.__name__} with values: {new_element}")
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
element = model(**new_element)
|
|
18
|
+
except Exception:
|
|
19
|
+
raise ValueError("Missing required parameters")
|
|
20
|
+
|
|
21
|
+
if "admin" not in user_roles:
|
|
22
|
+
model_name = model.__name__.lower()
|
|
23
|
+
access_rights_list = await cls.check_access_rights(model_name, user_roles, "create", crud_instance)
|
|
24
|
+
await cls.check_fields_permission(model, 'fields_create', new_element, access_rights_list)
|
|
25
|
+
|
|
26
|
+
created_element = await create_element(element, crud_instance)
|
|
27
|
+
|
|
28
|
+
if "admin" not in user_roles:
|
|
29
|
+
allowed_fields = cls.get_allowed_fields(access_rights_list, 'fields_visible')
|
|
30
|
+
created_element = {field: created_element[field] for field in allowed_fields if field in created_element}
|
|
31
|
+
|
|
32
|
+
_logger.info(f"{model.__name__} created successfully")
|
|
33
|
+
return created_element
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
async def update(cls, element_id:str, updated_values: dict, model: Type, user_roles: list, crud_instance: CRUD):
|
|
37
|
+
_logger.info(f"Updating {model.__name__} with ID: {element_id} and values: {updated_values}")
|
|
38
|
+
|
|
39
|
+
model_name = model.__name__.lower()
|
|
40
|
+
|
|
41
|
+
if "admin" not in user_roles:
|
|
42
|
+
access_rights_list = await cls.check_access_rights(model_name, user_roles, "update", crud_instance)
|
|
43
|
+
await cls.check_fields_permission(model, 'fields_edit', updated_values, access_rights_list)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
updated_element = await crud_instance.put_item(model_name, element_id, updated_values)
|
|
47
|
+
except Exception:
|
|
48
|
+
raise ValueError(f"{model.__name__} with ID does not exist, or the updating parameters have errors")
|
|
49
|
+
|
|
50
|
+
if "admin" not in user_roles:
|
|
51
|
+
allowed_fields = cls.get_allowed_fields(access_rights_list, 'fields_visible')
|
|
52
|
+
updated_element = {field: updated_element[field] for field in allowed_fields if field in updated_element}
|
|
53
|
+
|
|
54
|
+
_logger.info(f"{model.__name__} created successfully")
|
|
55
|
+
return updated_element
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
async def delete(cls, element_id: str, model: Type, user_roles: List[str], crud_instance: CRUD):
|
|
59
|
+
_logger.info(f"Deleting {model.__name__} with ID: {element_id}")
|
|
60
|
+
|
|
61
|
+
model_name = model.__name__.lower()
|
|
62
|
+
|
|
63
|
+
if "admin" not in user_roles:
|
|
64
|
+
await cls.check_access_rights(model_name, user_roles, "delete", crud_instance)
|
|
65
|
+
try:
|
|
66
|
+
await crud_instance.delete_item(model_name, element_id)
|
|
67
|
+
except Exception:
|
|
68
|
+
raise ValueError(f"{model.__name__} with ID does not exist, or there was an error deleting the element")
|
|
69
|
+
|
|
70
|
+
_logger.info(f"{model.__name__} deleted successfully")
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
async def read(cls, element_id: str, model: Type, user_roles: List[str], crud_instance: CRUD):
|
|
74
|
+
_logger.info(f"Getting {model.__name__} with ID: {element_id}")
|
|
75
|
+
|
|
76
|
+
model_name = model.__name__.lower()
|
|
77
|
+
|
|
78
|
+
if "admin" not in user_roles:
|
|
79
|
+
access_rights_list = await cls.check_access_rights(model_name, user_roles, "read", crud_instance)
|
|
80
|
+
try:
|
|
81
|
+
item = await crud_instance.get_item(model_name, element_id)
|
|
82
|
+
except ValueError as e:
|
|
83
|
+
raise ValueError(str(e))
|
|
84
|
+
|
|
85
|
+
if "admin" not in user_roles:
|
|
86
|
+
allowed_fields = cls.get_allowed_fields(access_rights_list, 'fields_visible')
|
|
87
|
+
item = {field: item[field] for field in allowed_fields if field in item}
|
|
88
|
+
|
|
89
|
+
_logger.info(f"{model.__name__} retrieved successfully")
|
|
90
|
+
return item
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
async def search(cls, filters: dict, model: Type, user_roles: List[str], crud_instance: CRUD):
|
|
94
|
+
_logger.info(f"Searching {model.__name__} with filters: {filters}")
|
|
95
|
+
|
|
96
|
+
model_name = model.__name__.lower()
|
|
97
|
+
|
|
98
|
+
if "admin" not in user_roles:
|
|
99
|
+
await cls.check_access_rights(model_name, user_roles, "search", crud_instance)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
items, total_count = await crud_instance.get_items(model_name, filters=filters)
|
|
103
|
+
except Exception:
|
|
104
|
+
raise ValueError(f"Error occurred while searching {model.__name__} with filters: {filters}")
|
|
105
|
+
|
|
106
|
+
_logger.info(f"{model.__name__} search completed successfully")
|
|
107
|
+
return items, total_count
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
async def check_access_rights(cls, model_name: str, roles: List[str], operation: str, crud_instance: CRUD):
|
|
111
|
+
access_rights_list = []
|
|
112
|
+
|
|
113
|
+
for role in roles:
|
|
114
|
+
access_rights, _ = await crud_instance.get_items(
|
|
115
|
+
"accessrights",
|
|
116
|
+
skip=0,
|
|
117
|
+
limit=1,
|
|
118
|
+
filters={
|
|
119
|
+
"model": model_name,
|
|
120
|
+
"role": role
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if access_rights and access_rights[0]["operations"].get(operation, 0) >= 1:
|
|
125
|
+
access_rights_list.append(access_rights[0])
|
|
126
|
+
|
|
127
|
+
if not access_rights_list:
|
|
128
|
+
raise PermissionError(f"None of the roles have sufficient permissions for operation '{operation}' on model '{model_name}'")
|
|
129
|
+
|
|
130
|
+
return access_rights_list
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
async def check_fields_permission(cls, model, fields_type: str, new_element: Dict[str, int], access_rights_list: List[AccessRights]):
|
|
134
|
+
model_fields = []
|
|
135
|
+
|
|
136
|
+
for class_in_hierarchy in model.mro():
|
|
137
|
+
if hasattr(class_in_hierarchy, '__annotations__'):
|
|
138
|
+
model_fields.extend([field for field in class_in_hierarchy.__annotations__ if not field.startswith("_")])
|
|
139
|
+
|
|
140
|
+
for field_name, field_value in new_element.items():
|
|
141
|
+
if field_name not in model_fields:
|
|
142
|
+
raise ValueError(f"Invalid field {field_name}")
|
|
143
|
+
|
|
144
|
+
for field_name, field_value in new_element.items():
|
|
145
|
+
allowed_by_some_role = False
|
|
146
|
+
|
|
147
|
+
for access_rights in access_rights_list:
|
|
148
|
+
if access_rights.get(fields_type).get(field_name, {}) == 1:
|
|
149
|
+
allowed_by_some_role = True
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
if not allowed_by_some_role:
|
|
153
|
+
raise PermissionError(f"Insufficient permissions to create the field '{field_name}' in any role.")
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def get_allowed_fields(cls, access_rights_list: List[AccessRights], fields_type: str):
|
|
157
|
+
allowed_fields = set()
|
|
158
|
+
|
|
159
|
+
for access_rights in access_rights_list:
|
|
160
|
+
fields = access_rights.get(fields_type, {})
|
|
161
|
+
for field_name, field_value in fields.items():
|
|
162
|
+
if field_value == 1:
|
|
163
|
+
allowed_fields.add(field_name)
|
|
164
|
+
|
|
165
|
+
return allowed_fields
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
from fastapi import FastAPI, HTTPException, status
|
|
3
|
+
from fastapi.routing import APIRouter
|
|
4
|
+
from typing import TypeVar, Optional
|
|
5
|
+
import re
|
|
6
|
+
from importlib.util import spec_from_file_location, module_from_spec
|
|
7
|
+
from .Route import Route
|
|
8
|
+
from ..crud.crud import CRUD
|
|
9
|
+
from .Model import LaiaBaseModel
|
|
10
|
+
from ..utils.logger import _logger
|
|
11
|
+
|
|
12
|
+
T = TypeVar('T', bound='LaiaBaseModel')
|
|
13
|
+
|
|
14
|
+
class OpenAPI:
|
|
15
|
+
def __init__(self, yaml_path):
|
|
16
|
+
self.yaml_path = yaml_path
|
|
17
|
+
self.routes = []
|
|
18
|
+
self.models = []
|
|
19
|
+
|
|
20
|
+
self.parse_yaml()
|
|
21
|
+
|
|
22
|
+
def parse_yaml(self):
|
|
23
|
+
with open(self.yaml_path, 'r') as file:
|
|
24
|
+
openapi_spec = yaml.safe_load(file)
|
|
25
|
+
|
|
26
|
+
if 'paths' in openapi_spec:
|
|
27
|
+
for path, path_data in openapi_spec['paths'].items():
|
|
28
|
+
methods = path_data.keys()
|
|
29
|
+
for method in methods:
|
|
30
|
+
if method != "parameters":
|
|
31
|
+
summary = path_data[method].get('summary', '')
|
|
32
|
+
responses = path_data[method].get('responses', {})
|
|
33
|
+
extensions = path_data[method].get('x-', {})
|
|
34
|
+
self.routes.append(Route(path, method, summary, responses, extensions))
|
|
35
|
+
|
|
36
|
+
if 'components' in openapi_spec:
|
|
37
|
+
schemas = openapi_spec['components'].get('schemas', {})
|
|
38
|
+
self.models = list(schemas.keys())
|
|
39
|
+
|
|
40
|
+
def create_crud_routes(self, api: FastAPI=None, crud_instance: CRUD=None, models_path: str=""):
|
|
41
|
+
for model_name in self.models:
|
|
42
|
+
model_module = self.import_model(models_path)
|
|
43
|
+
model = getattr(model_module, model_name)
|
|
44
|
+
model_lowercase = model_name.lower()
|
|
45
|
+
model_plural = model_lowercase + 's' # TODO: use it for search path
|
|
46
|
+
|
|
47
|
+
create_route = None
|
|
48
|
+
read_route = None
|
|
49
|
+
update_route = None
|
|
50
|
+
delete_route = None
|
|
51
|
+
search_route = None
|
|
52
|
+
|
|
53
|
+
for route in self.routes:
|
|
54
|
+
if route.extensions.get(f'x-create-{model_lowercase}'):
|
|
55
|
+
create_route = route.path
|
|
56
|
+
elif route.extensions.get(f'x-read-{model_lowercase}'):
|
|
57
|
+
read_route = route.path
|
|
58
|
+
elif route.extensions.get(f'x-update-{model_lowercase}'):
|
|
59
|
+
update_route = route.path
|
|
60
|
+
elif route.extensions.get(f'x-delete-{model_lowercase}'):
|
|
61
|
+
delete_route = route.path
|
|
62
|
+
elif route.extensions.get(f'x-search-{model_lowercase}'):
|
|
63
|
+
search_route = route.path
|
|
64
|
+
|
|
65
|
+
self.CRUD(
|
|
66
|
+
api=api,
|
|
67
|
+
model=model,
|
|
68
|
+
create_route=create_route,
|
|
69
|
+
read_route=read_route,
|
|
70
|
+
update_route=update_route,
|
|
71
|
+
delete_route=delete_route,
|
|
72
|
+
search_route=search_route
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def CRUD(self, api: FastAPI, model: T, create_route: Optional[str] = None, read_route: Optional[str] = None, update_route: Optional[str] = None, delete_route: Optional[str] = None, search_route: Optional[str] = None):
|
|
76
|
+
model_name = model.__name__.lower()
|
|
77
|
+
router = APIRouter(tags=[model.__name__])
|
|
78
|
+
|
|
79
|
+
def replace_placeholder(route: str) -> str:
|
|
80
|
+
return re.sub(r'\{.*?\}', '{element_id}', route)
|
|
81
|
+
|
|
82
|
+
create_route = create_route or f"/{model_name}/"
|
|
83
|
+
read_route = read_route or f"/{model_name}"+"/{element_id}"
|
|
84
|
+
update_route = update_route or f"/{model_name}"+"/{element_id}"
|
|
85
|
+
delete_route = delete_route or f"/{model_name}"+"/{element_id}"
|
|
86
|
+
search_route = search_route or None # TODO: change to the defined route
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@router.post(create_route, response_model=dict)
|
|
90
|
+
async def create_element(element: model):
|
|
91
|
+
user_roles=["admin"]
|
|
92
|
+
try:
|
|
93
|
+
return await model.create(dict(element), model, user_roles, self.crud_instance)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
96
|
+
|
|
97
|
+
@router.put(update_route, response_model=dict)
|
|
98
|
+
async def update_element(element_id: str, values: dict):
|
|
99
|
+
user_roles=["admin"]
|
|
100
|
+
try:
|
|
101
|
+
return await model.update(element_id, values, model, user_roles, self.crud_instance)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
104
|
+
|
|
105
|
+
@router.get(read_route, response_model=dict)
|
|
106
|
+
async def read_element(element_id: str):
|
|
107
|
+
user_roles=["admin"]
|
|
108
|
+
try:
|
|
109
|
+
return await model.read(element_id, model, user_roles, self.crud_instance)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
112
|
+
|
|
113
|
+
@router.delete(delete_route, response_model=str)
|
|
114
|
+
async def delete_element(element_id: str):
|
|
115
|
+
user_roles=["admin"]
|
|
116
|
+
try:
|
|
117
|
+
await model.delete(element_id, model, user_roles, self.crud_instance)
|
|
118
|
+
return f"{model_name} element deleted successfully"
|
|
119
|
+
except Exception as e:
|
|
120
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
121
|
+
|
|
122
|
+
api.include_router(router)
|
|
123
|
+
|
|
124
|
+
def import_model(self, models_path):
|
|
125
|
+
spec = spec_from_file_location("models", models_path)
|
|
126
|
+
module = module_from_spec(spec)
|
|
127
|
+
spec.loader.exec_module(module)
|
|
128
|
+
return module
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from ..crud.crud import CRUD
|
|
4
|
+
from ..utils.logger import _logger
|
|
5
|
+
from ..utils.utils import create_element
|
|
6
|
+
|
|
7
|
+
class Role(BaseModel):
|
|
8
|
+
name: str
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
async def create(cls, new_role: dict, user_roles: list, crud_instance: CRUD):
|
|
12
|
+
_logger.info(f"Creating new Role with values: {new_role}")
|
|
13
|
+
|
|
14
|
+
if "name" not in new_role:
|
|
15
|
+
raise ValueError("Missing required parameter: name")
|
|
16
|
+
|
|
17
|
+
role = Role(**new_role)
|
|
18
|
+
if "admin" not in user_roles:
|
|
19
|
+
raise PermissionError("Only users with 'admin' role can create new roles")
|
|
20
|
+
|
|
21
|
+
existing_roles, _ = await crud_instance.get_items("role", skip=0, limit=10, filters={"name": role.name})
|
|
22
|
+
if existing_roles:
|
|
23
|
+
raise ValueError(f"Role with name '{role.name}' already exists")
|
|
24
|
+
|
|
25
|
+
created_role = await create_element(role, crud_instance)
|
|
26
|
+
_logger.info("Role created successfully")
|
|
27
|
+
return cls(**created_role)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class Route:
|
|
2
|
+
def __init__(self, path, method, summary, responses, extensions):
|
|
3
|
+
self.path = path
|
|
4
|
+
self.method = method
|
|
5
|
+
self.summary = summary
|
|
6
|
+
self.responses = responses
|
|
7
|
+
self.extensions = extensions
|
|
8
|
+
|
|
9
|
+
def __str__(self):
|
|
10
|
+
return f"Path: {self.path}, Method: {self.method}, Summary: {self.summary}, Responses: {self.responses}, Extensions: {self.extensions}"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
class CustomFormatter(logging.Formatter):
|
|
4
|
+
|
|
5
|
+
purple = "\x1b[35;20m"
|
|
6
|
+
green = "\x1b[32;20m"
|
|
7
|
+
yellow = "\x1b[33;20m"
|
|
8
|
+
red = "\x1b[31;20m"
|
|
9
|
+
bold_red = "\x1b[31;1m"
|
|
10
|
+
reset = "\x1b[0m"
|
|
11
|
+
format = "%(asctime)s - (%(filename)s:%(lineno)d) - %(levelname)s - %(message)s "
|
|
12
|
+
|
|
13
|
+
FORMATS = {
|
|
14
|
+
logging.DEBUG: purple + format + reset,
|
|
15
|
+
logging.INFO: green + format + reset,
|
|
16
|
+
logging.WARNING: yellow + format + reset,
|
|
17
|
+
logging.ERROR: red + format + reset,
|
|
18
|
+
logging.CRITICAL: bold_red + format + reset
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def format(self, record):
|
|
22
|
+
log_fmt = self.FORMATS.get(record.levelno, self.format)
|
|
23
|
+
formatter = logging.Formatter(log_fmt)
|
|
24
|
+
return formatter.format(record)
|
|
25
|
+
|
|
26
|
+
_logger = logging.getLogger("LAIA")
|
|
27
|
+
_logger.setLevel(logging.INFO)
|
|
28
|
+
|
|
29
|
+
ch = logging.StreamHandler()
|
|
30
|
+
ch.setLevel(logging.DEBUG)
|
|
31
|
+
|
|
32
|
+
ch.setFormatter(CustomFormatter())
|
|
33
|
+
|
|
34
|
+
_logger.addHandler(ch)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
import subprocess
|
|
3
|
+
import re
|
|
4
|
+
from ..crud.crud import CRUD
|
|
5
|
+
from .logger import _logger
|
|
6
|
+
|
|
7
|
+
T = TypeVar('T', bound='BaseModel')
|
|
8
|
+
|
|
9
|
+
def create_models_file(input_file="openapi.yaml", output_file="model.py"):
|
|
10
|
+
# This function uses the datamodel-code-generator for generating the pydantic models given a openapi.yaml file.
|
|
11
|
+
# The generated file is modified so that the pydantic models extend the LaiaBaseModel, this is necessary for
|
|
12
|
+
# using the Laia library
|
|
13
|
+
|
|
14
|
+
subprocess.run(["datamodel-codegen", "--input", input_file, "--output", output_file], check=True)
|
|
15
|
+
|
|
16
|
+
import_statement = """
|
|
17
|
+
# modified by laia-gen-lib:
|
|
18
|
+
|
|
19
|
+
from laiagenlib.models.Model import LaiaBaseModel"""
|
|
20
|
+
|
|
21
|
+
with open(output_file, 'r') as f:
|
|
22
|
+
model_content = f.read()
|
|
23
|
+
|
|
24
|
+
lines = model_content.split('\n')
|
|
25
|
+
import_index = next((i for i, line in enumerate(lines) if "from __future__ import annotations" in line), None)
|
|
26
|
+
|
|
27
|
+
if import_index is not None:
|
|
28
|
+
lines.insert(import_index + 1, import_statement)
|
|
29
|
+
|
|
30
|
+
modified_content = '\n'.join(lines)
|
|
31
|
+
modified_content = re.sub(r'class\s+(\w+)\(BaseModel\):', r'class \1(LaiaBaseModel):', modified_content)
|
|
32
|
+
|
|
33
|
+
with open(output_file, 'w') as f:
|
|
34
|
+
f.write(modified_content)
|
|
35
|
+
|
|
36
|
+
print(f"File '{output_file}' created and modified.")
|
|
37
|
+
|
|
38
|
+
async def create_element(element: T, crud_instance: CRUD):
|
|
39
|
+
model_name = element.__class__.__name__.lower()
|
|
40
|
+
|
|
41
|
+
return await crud_instance.post_item(model_name, element)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='laia-gen-lib',
|
|
5
|
+
packages=find_packages(),
|
|
6
|
+
version='0.1.0',
|
|
7
|
+
description='An AI application generator engine',
|
|
8
|
+
author='Me',
|
|
9
|
+
install_requires=['pymongo', 'pydantic', 'datamodel-code-generator'],
|
|
10
|
+
setup_requires=['pytest-runner'],
|
|
11
|
+
tests_require=['pytest', 'pymongo', 'pytest-asyncio'],
|
|
12
|
+
test_suite='tests',
|
|
13
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pytest_asyncio
|
|
3
|
+
from pymongo import MongoClient
|
|
4
|
+
from laiagenlib.crud.crud_mongo_impl import CRUDMongoImpl
|
|
5
|
+
from laiagenlib.models.AccessRights import AccessRights
|
|
6
|
+
from laiagenlib.models.Model import LaiaBaseModel
|
|
7
|
+
|
|
8
|
+
class User(LaiaBaseModel):
|
|
9
|
+
description: str
|
|
10
|
+
age: int
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def in_memory_db():
|
|
14
|
+
client = MongoClient()
|
|
15
|
+
db = client["testdb"]
|
|
16
|
+
db.drop_collection("accessrights")
|
|
17
|
+
return db
|
|
18
|
+
|
|
19
|
+
@pytest_asyncio.fixture
|
|
20
|
+
async def crud_instance(in_memory_db):
|
|
21
|
+
return CRUDMongoImpl(in_memory_db)
|
|
22
|
+
|
|
23
|
+
class TestAccessRights:
|
|
24
|
+
|
|
25
|
+
@pytest.mark.asyncio
|
|
26
|
+
async def test_model_name_different_that_model_raises_exception(self, crud_instance):
|
|
27
|
+
user_roles = ["admin"]
|
|
28
|
+
new_access_rights = {
|
|
29
|
+
"role": "owner",
|
|
30
|
+
"model": "drone",
|
|
31
|
+
"operations": {"create": 1, "read": 1},
|
|
32
|
+
"fields_create": {"description": 1},
|
|
33
|
+
"fields_edit": {},
|
|
34
|
+
"fields_visible": {"description": 1}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
with pytest.raises(ValueError, match="Provided model name does not match the class model name"):
|
|
38
|
+
await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_create_access_rights_no_admin_permission_raises_exception(self, crud_instance):
|
|
42
|
+
user_roles = ["user"]
|
|
43
|
+
new_access_rights = {
|
|
44
|
+
"role": "userAdmin",
|
|
45
|
+
"model": "user",
|
|
46
|
+
"operations": {"create": 1, "read": 1},
|
|
47
|
+
"fields_create": {"description": 1},
|
|
48
|
+
"fields_edit": {},
|
|
49
|
+
"fields_visible": {"description": 1}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
with pytest.raises(PermissionError, match="Only users with 'admin' role can create access rights"):
|
|
53
|
+
await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
|
|
54
|
+
|
|
55
|
+
@pytest.mark.asyncio
|
|
56
|
+
async def test_create_access_rights_admin_permission(self, crud_instance):
|
|
57
|
+
user_roles = ["admin"]
|
|
58
|
+
new_access_rights = {
|
|
59
|
+
"role": "userAdmin",
|
|
60
|
+
"model": "user",
|
|
61
|
+
"operations": {"create": 1, "read": 1},
|
|
62
|
+
"fields_create": {"description": 1},
|
|
63
|
+
"fields_edit": {"name": 1},
|
|
64
|
+
"fields_visible": {"description": 1}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
created_access_rights = await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
|
|
68
|
+
|
|
69
|
+
assert created_access_rights.role == "userAdmin"
|
|
70
|
+
assert created_access_rights.model == "user"
|
|
71
|
+
|
|
72
|
+
@pytest.mark.asyncio
|
|
73
|
+
async def test_create_access_rights_admin_permission(self, crud_instance):
|
|
74
|
+
user_roles = ["admin"]
|
|
75
|
+
new_access_rights = {
|
|
76
|
+
"role": "userAdmin",
|
|
77
|
+
"operations": {"create": 1, "read": 1},
|
|
78
|
+
"fields_create": {"description": 1},
|
|
79
|
+
"fields_edit": {"name": 1},
|
|
80
|
+
"fields_visible": {"description": 1}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
with pytest.raises(ValueError, match="Missing required parameters"):
|
|
84
|
+
await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
|
|
85
|
+
|
|
86
|
+
@pytest.mark.asyncio
|
|
87
|
+
async def test_create_access_rights_invalid_operations(self, crud_instance):
|
|
88
|
+
user_roles = ["admin"]
|
|
89
|
+
new_access_rights = {
|
|
90
|
+
"role": "userAdmin",
|
|
91
|
+
"model": "user",
|
|
92
|
+
"operations": {"invalid_op": 1},
|
|
93
|
+
"fields_create": {"description": 1},
|
|
94
|
+
"fields_edit": {"name": 1},
|
|
95
|
+
"fields_visible": {"description": 1}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
with pytest.raises(ValueError, match="Invalid format for operation invalid_op"):
|
|
99
|
+
await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
|
|
100
|
+
|
|
101
|
+
@pytest.mark.asyncio
|
|
102
|
+
async def test_create_access_rights_invalid_fields_format(self, crud_instance):
|
|
103
|
+
user_roles = ["admin"]
|
|
104
|
+
new_access_rights = {
|
|
105
|
+
"role": "userAdmin",
|
|
106
|
+
"model": "user",
|
|
107
|
+
"operations": {"create": 1, "read": 1},
|
|
108
|
+
"fields_create": "invalid_format",
|
|
109
|
+
"fields_edit": {"name": 1},
|
|
110
|
+
"fields_visible": {"description": 1}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
with pytest.raises(ValueError, match="Invalid format for fields_create"):
|
|
114
|
+
await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
|
|
115
|
+
|
|
116
|
+
@pytest.mark.asyncio
|
|
117
|
+
async def test_create_access_rights_invalid_field_name(self, crud_instance):
|
|
118
|
+
user_roles = ["admin"]
|
|
119
|
+
new_access_rights = {
|
|
120
|
+
"role": "userAdmin",
|
|
121
|
+
"model": "user",
|
|
122
|
+
"operations": {"create": 1, "read": 1},
|
|
123
|
+
"fields_create": {"invalid_field": 1},
|
|
124
|
+
"fields_edit": {"name": 1},
|
|
125
|
+
"fields_visible": {"description": 1}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
with pytest.raises(ValueError, match="Invalid field invalid_field for fields_create"):
|
|
129
|
+
await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
|
|
130
|
+
|
|
131
|
+
@pytest.mark.asyncio
|
|
132
|
+
async def test_create_access_rights_already_exists(self, crud_instance):
|
|
133
|
+
user_roles = ["admin"]
|
|
134
|
+
existing_access_rights = {
|
|
135
|
+
"role": "userAdmin",
|
|
136
|
+
"model": "user",
|
|
137
|
+
"operations": {"create": 1, "read": 1},
|
|
138
|
+
"fields_create": {"description": 1},
|
|
139
|
+
"fields_edit": {"name": 1},
|
|
140
|
+
"fields_visible": {"description": 1}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await AccessRights.create(existing_access_rights, User, user_roles, crud_instance)
|
|
144
|
+
|
|
145
|
+
new_access_rights = {
|
|
146
|
+
"role": "userAdmin",
|
|
147
|
+
"model": "user",
|
|
148
|
+
"operations": {"create": 1, "read": 1},
|
|
149
|
+
"fields_create": {"description": 1},
|
|
150
|
+
"fields_edit": {"name": 1},
|
|
151
|
+
"fields_visible": {"description": 1}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
with pytest.raises(ValueError, match="AccessRights with the same role and model already exists"):
|
|
155
|
+
await AccessRights.create(new_access_rights, User, user_roles, crud_instance)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pytest_asyncio
|
|
3
|
+
from pymongo import MongoClient
|
|
4
|
+
from laiagenlib.crud.crud_mongo_impl import CRUDMongoImpl
|
|
5
|
+
from laiagenlib.models.Model import LaiaBaseModel
|
|
6
|
+
from laiagenlib.models.AccessRights import AccessRights
|
|
7
|
+
from laiagenlib.utils.logger import _logger
|
|
8
|
+
|
|
9
|
+
class User(LaiaBaseModel):
|
|
10
|
+
description: str
|
|
11
|
+
age: int
|
|
12
|
+
|
|
13
|
+
class Drone(LaiaBaseModel):
|
|
14
|
+
description: str
|
|
15
|
+
weight: float
|
|
16
|
+
max_altitude: float
|
|
17
|
+
max_speed: float
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def in_memory_db():
|
|
21
|
+
client = MongoClient()
|
|
22
|
+
db = client["testdb"]
|
|
23
|
+
db.drop_collection("user")
|
|
24
|
+
db.drop_collection("drone")
|
|
25
|
+
db.drop_collection("accessrights")
|
|
26
|
+
return db
|
|
27
|
+
|
|
28
|
+
@pytest_asyncio.fixture
|
|
29
|
+
async def crud_instance(in_memory_db):
|
|
30
|
+
return CRUDMongoImpl(in_memory_db)
|
|
31
|
+
|
|
32
|
+
class TestModel:
|
|
33
|
+
|
|
34
|
+
@pytest.mark.asyncio
|
|
35
|
+
async def test_model_created_by_admin(self, crud_instance):
|
|
36
|
+
user_roles = ["admin"]
|
|
37
|
+
new_user = {
|
|
38
|
+
"name": "alba",
|
|
39
|
+
"description": "This is a description",
|
|
40
|
+
"age": 21
|
|
41
|
+
}
|
|
42
|
+
await User.create(new_user, User, user_roles, crud_instance)
|
|
43
|
+
|
|
44
|
+
@pytest.mark.asyncio
|
|
45
|
+
async def test_create_element_missing_parameters_raises_exception(self, crud_instance):
|
|
46
|
+
user_roles = ["admin"]
|
|
47
|
+
new_user = {
|
|
48
|
+
"name": "alba",
|
|
49
|
+
"description": "This is a description",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
with pytest.raises(ValueError, match="Missing required parameters"):
|
|
53
|
+
await User.create(new_user, User, user_roles, crud_instance)
|
|
54
|
+
|
|
55
|
+
@pytest.mark.asyncio
|
|
56
|
+
async def test_create_element_missing_access_rights(self, crud_instance):
|
|
57
|
+
user_roles = ["userAdmin"]
|
|
58
|
+
new_user = {
|
|
59
|
+
"name": "alba",
|
|
60
|
+
"description": "This is a description",
|
|
61
|
+
"age": 21
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
with pytest.raises(PermissionError, match="None of the roles have sufficient permissions for operation 'create' on model 'user'"):
|
|
65
|
+
await User.create(new_user, User, user_roles, crud_instance)
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_create_element_missing_access_rights_create_operation(self, crud_instance):
|
|
69
|
+
new_access_rights = {
|
|
70
|
+
"role": "userAdmin",
|
|
71
|
+
"model": "user",
|
|
72
|
+
"operations": {"read": 1},
|
|
73
|
+
"fields_create": {"description": 1},
|
|
74
|
+
"fields_edit": {"name": 1},
|
|
75
|
+
"fields_visible": {"description": 1}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await AccessRights.create(new_access_rights, User, ["admin"], crud_instance)
|
|
79
|
+
|
|
80
|
+
user_roles = ["userAdmin"]
|
|
81
|
+
new_user = {
|
|
82
|
+
"name": "alba",
|
|
83
|
+
"description": "This is a description",
|
|
84
|
+
"age": 21
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
with pytest.raises(PermissionError, match="None of the roles have sufficient permissions for operation 'create' on model 'user'"):
|
|
88
|
+
await User.create(new_user, User, user_roles, crud_instance)
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_create_element_missing_access_rights_specific_field(self, crud_instance):
|
|
92
|
+
new_access_rights = {
|
|
93
|
+
"role": "userAdmin",
|
|
94
|
+
"model": "user",
|
|
95
|
+
"operations": {"create": 1, "read": 1},
|
|
96
|
+
"fields_create": {"name": 1, "description": 1},
|
|
97
|
+
"fields_edit": {"name": 1},
|
|
98
|
+
"fields_visible": {"description": 1}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await AccessRights.create(new_access_rights, User, ["admin"], crud_instance)
|
|
102
|
+
|
|
103
|
+
user_roles = ["userAdmin"]
|
|
104
|
+
new_user = {
|
|
105
|
+
"name": "alba",
|
|
106
|
+
"description": "This is a description",
|
|
107
|
+
"age": 21
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
with pytest.raises(PermissionError, match="Insufficient permissions to create the field 'age' in any role."):
|
|
111
|
+
await User.create(new_user, User, user_roles, crud_instance)
|
|
112
|
+
|
|
113
|
+
@pytest.mark.asyncio
|
|
114
|
+
async def test_create_element_with_different_role(self, crud_instance):
|
|
115
|
+
new_access_rights1 = {
|
|
116
|
+
"role": "userAdmin1",
|
|
117
|
+
"model": "user",
|
|
118
|
+
"operations": {"create": 1, "read": 1},
|
|
119
|
+
"fields_create": {"name": 1, "description": 1},
|
|
120
|
+
"fields_edit": {"name": 1},
|
|
121
|
+
"fields_visible": {"description": 1}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
new_access_rights2 = {
|
|
125
|
+
"role": "userAdmin2",
|
|
126
|
+
"model": "user",
|
|
127
|
+
"operations": {"create": 1, "read": 1},
|
|
128
|
+
"fields_create": {"name": 1, "description": 1, "age": 1},
|
|
129
|
+
"fields_edit": {"name": 1},
|
|
130
|
+
"fields_visible": {"description": 1}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
await AccessRights.create(new_access_rights1, User, ["admin"], crud_instance)
|
|
135
|
+
await AccessRights.create(new_access_rights2, User, ["admin"], crud_instance)
|
|
136
|
+
|
|
137
|
+
user_roles = ["userAdmin1", "userAdmin2"]
|
|
138
|
+
new_user = {
|
|
139
|
+
"name": "alba",
|
|
140
|
+
"description": "This is a description",
|
|
141
|
+
"age": 21
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
user = await User.create(new_user, User, user_roles, crud_instance)
|
|
145
|
+
|
|
146
|
+
assert user is not None
|
|
147
|
+
assert user["description"] == "This is a description"
|
|
148
|
+
|
|
149
|
+
@pytest.mark.asyncio
|
|
150
|
+
async def test_model_updated_by_admin(self, crud_instance):
|
|
151
|
+
user_roles = ["admin"]
|
|
152
|
+
new_user = {
|
|
153
|
+
"name": "alba",
|
|
154
|
+
"description": "This is a description",
|
|
155
|
+
"age": 21
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
user = await User.create(new_user, User, user_roles, crud_instance)
|
|
159
|
+
|
|
160
|
+
updated_user = await User.update(user["id"], {"name": "blanca"}, User, user_roles, crud_instance)
|
|
161
|
+
|
|
162
|
+
assert updated_user is not None
|
|
163
|
+
assert updated_user["name"] == "blanca"
|
|
164
|
+
assert updated_user["description"] == user["description"]
|
|
165
|
+
assert updated_user["age"] == user["age"]
|
|
166
|
+
assert updated_user["id"] == user["id"]
|
|
167
|
+
|
|
168
|
+
@pytest.mark.asyncio
|
|
169
|
+
async def test_read_element_missing_access_rights_read_operation(self, crud_instance):
|
|
170
|
+
new_access_rights = {
|
|
171
|
+
"role": "userAdmin",
|
|
172
|
+
"model": "user",
|
|
173
|
+
"operations": {"create": 1, "update": 1},
|
|
174
|
+
"fields_create": {"name": 1, "description": 1},
|
|
175
|
+
"fields_edit": {"name": 1},
|
|
176
|
+
"fields_visible": {"description": 1}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await AccessRights.create(new_access_rights, User, ["admin"], crud_instance)
|
|
180
|
+
|
|
181
|
+
user_roles = ["userAdmin"]
|
|
182
|
+
new_user = {
|
|
183
|
+
"name": "alba",
|
|
184
|
+
"description": "This is a description",
|
|
185
|
+
"age": 21
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
user = await User.create(new_user, User, ["admin"], crud_instance)
|
|
189
|
+
|
|
190
|
+
with pytest.raises(PermissionError, match="None of the roles have sufficient permissions for operation 'read' on model 'user'"):
|
|
191
|
+
await User.read(user["id"], User, user_roles, crud_instance)
|
|
192
|
+
|
|
193
|
+
@pytest.mark.asyncio
|
|
194
|
+
async def test_read_element_missing_access_rights_specific_field(self, crud_instance):
|
|
195
|
+
new_access_rights = {
|
|
196
|
+
"role": "userAdmin",
|
|
197
|
+
"model": "user",
|
|
198
|
+
"operations": {"create": 1, "read": 1, "update": 1},
|
|
199
|
+
"fields_create": {"name": 1, "description": 1},
|
|
200
|
+
"fields_edit": {"name": 1},
|
|
201
|
+
"fields_visible": {"description": 1}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await AccessRights.create(new_access_rights, User, ["admin"], crud_instance)
|
|
205
|
+
|
|
206
|
+
user_roles = ["userAdmin"]
|
|
207
|
+
new_user = {
|
|
208
|
+
"name": "alba",
|
|
209
|
+
"description": "This is a description",
|
|
210
|
+
"age": 21
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
user = await User.create(new_user, User, ["admin"], crud_instance)
|
|
214
|
+
|
|
215
|
+
user_read = await User.read(user["id"], User, user_roles, crud_instance)
|
|
216
|
+
|
|
217
|
+
assert user_read == {"description": "This is a description"}
|
|
218
|
+
|
|
219
|
+
@pytest.mark.asyncio
|
|
220
|
+
async def test_delete_element(self, crud_instance):
|
|
221
|
+
user_roles = ["admin"]
|
|
222
|
+
new_user = {
|
|
223
|
+
"name": "alba",
|
|
224
|
+
"description": "This is a description",
|
|
225
|
+
"age": 21
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
user = await User.create(new_user, User, user_roles, crud_instance)
|
|
229
|
+
|
|
230
|
+
await User.delete(user["id"], User, user_roles, crud_instance)
|
|
231
|
+
|
|
232
|
+
with pytest.raises(ValueError, match=f"user with ID {user['id']} not found"):
|
|
233
|
+
await User.read(user["id"], User, user_roles, crud_instance)
|
|
234
|
+
|
|
235
|
+
@pytest.mark.asyncio
|
|
236
|
+
async def test_delete_element_missing_access_rights_delete_operation(self, crud_instance):
|
|
237
|
+
new_access_rights = {
|
|
238
|
+
"role": "userAdmin",
|
|
239
|
+
"model": "user",
|
|
240
|
+
"operations": {"create": 1, "update": 1},
|
|
241
|
+
"fields_create": {"name": 1, "description": 1},
|
|
242
|
+
"fields_edit": {"name": 1},
|
|
243
|
+
"fields_visible": {"description": 1}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await AccessRights.create(new_access_rights, User, ["admin"], crud_instance)
|
|
247
|
+
|
|
248
|
+
user_roles = ["userAdmin"]
|
|
249
|
+
new_user = {
|
|
250
|
+
"name": "alba",
|
|
251
|
+
"description": "This is a description",
|
|
252
|
+
"age": 21
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
user = await User.create(new_user, User, ["admin"], crud_instance)
|
|
256
|
+
|
|
257
|
+
with pytest.raises(PermissionError, match="None of the roles have sufficient permissions for operation 'delete' on model 'user'"):
|
|
258
|
+
await User.delete(user["id"], User, user_roles, crud_instance)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import re
|
|
3
|
+
import os
|
|
4
|
+
from laiagenlib.models.Openapi import OpenAPI
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def test_files(request):
|
|
8
|
+
test_dir = os.path.dirname(os.path.abspath(request.module.__file__))
|
|
9
|
+
input_file1 = os.path.join(test_dir, "..", "openapi_files", "api.yaml")
|
|
10
|
+
input_file2 = os.path.join(test_dir, "..", "openapi_files", "api2.yaml")
|
|
11
|
+
input_file3 = os.path.join(test_dir, "..", "openapi_files", "api3.yaml")
|
|
12
|
+
yield input_file1, input_file2, input_file3
|
|
13
|
+
|
|
14
|
+
def test_create_models_file(test_files):
|
|
15
|
+
input_file1, input_file2, input_file3 = test_files
|
|
16
|
+
|
|
17
|
+
parser = OpenAPI(input_file1)
|
|
18
|
+
assert len(parser.routes) == 8
|
|
19
|
+
|
|
20
|
+
parser2 = OpenAPI(input_file2)
|
|
21
|
+
assert len(parser2.routes) == 6
|
|
22
|
+
|
|
23
|
+
parser3 = OpenAPI(input_file3)
|
|
24
|
+
assert len(parser3.routes) == 1
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pytest_asyncio
|
|
3
|
+
from pymongo import MongoClient
|
|
4
|
+
from laiagenlib.crud.crud_mongo_impl import CRUDMongoImpl
|
|
5
|
+
from laiagenlib.models.Role import Role
|
|
6
|
+
|
|
7
|
+
@pytest.fixture
|
|
8
|
+
def in_memory_db():
|
|
9
|
+
client = MongoClient()
|
|
10
|
+
db = client["testdb"]
|
|
11
|
+
db.drop_collection("role")
|
|
12
|
+
return db
|
|
13
|
+
|
|
14
|
+
@pytest_asyncio.fixture
|
|
15
|
+
async def crud_instance(in_memory_db):
|
|
16
|
+
return CRUDMongoImpl(in_memory_db)
|
|
17
|
+
|
|
18
|
+
class TestRole:
|
|
19
|
+
|
|
20
|
+
@pytest.mark.asyncio
|
|
21
|
+
async def test_create_role_without_name_raises_exception(self, crud_instance):
|
|
22
|
+
user_roles = ["admin"]
|
|
23
|
+
new_role = {"something": "test_role"}
|
|
24
|
+
|
|
25
|
+
with pytest.raises(ValueError, match="Missing required parameter: name"):
|
|
26
|
+
await Role.create(new_role, user_roles, crud_instance)
|
|
27
|
+
|
|
28
|
+
@pytest.mark.asyncio
|
|
29
|
+
async def test_create_role_admin_permission(self, crud_instance):
|
|
30
|
+
user_roles = ["admin"]
|
|
31
|
+
new_role = {"name": "test_role"}
|
|
32
|
+
|
|
33
|
+
created_role = await Role.create(new_role, user_roles, crud_instance)
|
|
34
|
+
|
|
35
|
+
assert created_role.name == "test_role"
|
|
36
|
+
|
|
37
|
+
found_roles, _ = await crud_instance.get_items("role", filters={"name": "test_role"})
|
|
38
|
+
|
|
39
|
+
assert len(found_roles) == 1
|
|
40
|
+
assert found_roles[0]["name"] == "test_role"
|
|
41
|
+
|
|
42
|
+
@pytest.mark.asyncio
|
|
43
|
+
async def test_create_role_different_permission(self, crud_instance):
|
|
44
|
+
user_roles = ["user"]
|
|
45
|
+
new_role = {"name": "test_role"}
|
|
46
|
+
|
|
47
|
+
with pytest.raises(PermissionError, match="Only users with 'admin' role can create new roles"):
|
|
48
|
+
await Role.create(new_role, user_roles, crud_instance)
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_create_role_duplicate_name(self, crud_instance):
|
|
52
|
+
user_roles = ["admin"]
|
|
53
|
+
new_role = {"name": "test_role"}
|
|
54
|
+
|
|
55
|
+
await Role.create(new_role, user_roles, crud_instance)
|
|
56
|
+
|
|
57
|
+
with pytest.raises(ValueError, match=f"Role with name '{new_role['name']}' already exists"):
|
|
58
|
+
await Role.create(new_role, user_roles, crud_instance)
|