feat: adapt UGs to roles

This commit is contained in:
swve 2024-03-28 11:20:42 +00:00
parent 0df250c729
commit a6152ef1f5
9 changed files with 265 additions and 70 deletions

View file

@ -19,6 +19,7 @@ class Permission(BaseModel):
class Rights(BaseModel): class Rights(BaseModel):
courses: Permission courses: Permission
users: Permission users: Permission
usergroups : Permission
collections: Permission collections: Permission
organizations: Permission organizations: Permission
coursechapters: Permission coursechapters: Permission

View file

@ -3,12 +3,12 @@ from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
class UserGroupRessource(SQLModel, table=True): class UserGroupResource(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
usergroup_id: int = Field( usergroup_id: int = Field(
sa_column=Column(Integer, ForeignKey("usergroup.id", ondelete="CASCADE")) sa_column=Column(Integer, ForeignKey("usergroup.id", ondelete="CASCADE"))
) )
ressource_uuid: str = "" resource_uuid: str = ""
org_id: int = Field( org_id: int = Field(
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE")) sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
) )

View file

@ -5,12 +5,13 @@ from src.services.users.users import delete_user_by_id
from src.db.usergroups import UserGroupCreate, UserGroupRead, UserGroupUpdate from src.db.usergroups import UserGroupCreate, UserGroupRead, UserGroupUpdate
from src.db.users import PublicUser from src.db.users import PublicUser
from src.services.users.usergroups import ( from src.services.users.usergroups import (
add_ressources_to_usergroup, add_resources_to_usergroup,
add_users_to_usergroup, add_users_to_usergroup,
create_usergroup, create_usergroup,
delete_usergroup_by_id, delete_usergroup_by_id,
read_usergroup_by_id, read_usergroup_by_id,
remove_ressources_from_usergroup, read_usergroups_by_org_id,
remove_resources_from_usergroup,
remove_users_from_usergroup, remove_users_from_usergroup,
update_usergroup_by_id, update_usergroup_by_id,
) )
@ -50,6 +51,20 @@ async def api_get_usergroup(
return await read_usergroup_by_id(request, db_session, current_user, usergroup_id) return await read_usergroup_by_id(request, db_session, current_user, usergroup_id)
@router.get("/org/{org_id}", response_model=list[UserGroupRead], tags=["usergroups"])
async def api_get_usergroups(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
org_id: int,
) -> list[UserGroupRead]:
"""
Get UserGroups by Org
"""
return await read_usergroups_by_org_id(request, db_session, current_user, org_id)
@router.put("/{usergroup_id}", response_model=UserGroupRead, tags=["usergroups"]) @router.put("/{usergroup_id}", response_model=UserGroupRead, tags=["usergroups"])
async def api_update_usergroup( async def api_update_usergroup(
*, *,
@ -115,35 +130,35 @@ async def api_delete_users_from_usergroup(
) )
@router.post("/{usergroup_id}/add_ressources", tags=["usergroups"]) @router.post("/{usergroup_id}/add_resources", tags=["usergroups"])
async def api_add_ressources_to_usergroup( async def api_add_resources_to_usergroup(
*, *,
request: Request, request: Request,
db_session: Session = Depends(get_db_session), db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
usergroup_id: int, usergroup_id: int,
ressource_uuids: str, resource_uuids: str,
) -> str: ) -> str:
""" """
Add Ressources to UserGroup Add Resources to UserGroup
""" """
return await add_ressources_to_usergroup( return await add_resources_to_usergroup(
request, db_session, current_user, usergroup_id, ressource_uuids request, db_session, current_user, usergroup_id, resource_uuids
) )
@router.delete("/{usergroup_id}/remove_ressources", tags=["usergroups"]) @router.delete("/{usergroup_id}/remove_resources", tags=["usergroups"])
async def api_delete_ressources_from_usergroup( async def api_delete_resources_from_usergroup(
*, *,
request: Request, request: Request,
db_session: Session = Depends(get_db_session), db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
usergroup_id: int, usergroup_id: int,
ressource_uuids: str, resource_uuids: str,
) -> str: ) -> str:
""" """
Delete Ressources from UserGroup Delete Resources from UserGroup
""" """
return await remove_ressources_from_usergroup( return await remove_resources_from_usergroup(
request, db_session, current_user, usergroup_id, ressource_uuids request, db_session, current_user, usergroup_id, resource_uuids
) )

View file

@ -1,25 +1,27 @@
from fastapi import HTTPException, status from fastapi import HTTPException, status
async def check_element_type(element_id): async def check_element_type(element_uuid):
""" """
Check if the element is a course, a user, a house or a collection, by checking its prefix Check if the element is a course, a user, a house or a collection, by checking its prefix
""" """
if element_id.startswith("course_"): if element_uuid.startswith("course_"):
return "courses" return "courses"
elif element_id.startswith("user_"): elif element_uuid.startswith("user_"):
return "users" return "users"
elif element_id.startswith("house_"): elif element_uuid.startswith("usergroup_"):
return "usergroups"
elif element_uuid.startswith("house_"):
return "houses" return "houses"
elif element_id.startswith("org_"): elif element_uuid.startswith("org_"):
return "organizations" return "organizations"
elif element_id.startswith("chapter_"): elif element_uuid.startswith("chapter_"):
return "coursechapters" return "coursechapters"
elif element_id.startswith("collection_"): elif element_uuid.startswith("collection_"):
return "collections" return "collections"
elif element_id.startswith("activity_"): elif element_uuid.startswith("activity_"):
return "activities" return "activities"
elif element_id.startswith("role_"): elif element_uuid.startswith("role_"):
return "roles" return "roles"
else: else:
raise HTTPException( raise HTTPException(
@ -28,8 +30,8 @@ async def check_element_type(element_id):
) )
async def get_singular_form_of_element(element_id): async def get_singular_form_of_element(element_uuid):
element_type = await check_element_type(element_id) element_type = await check_element_type(element_uuid)
if element_type == "activities": if element_type == "activities":
return "activity" return "activity"
@ -38,8 +40,8 @@ async def get_singular_form_of_element(element_id):
return singular_form_element return singular_form_element
async def get_id_identifier_of_element(element_id): async def get_id_identifier_of_element(element_uuid):
singular_form_element = await get_singular_form_of_element(element_id) singular_form_element = await get_singular_form_of_element(element_uuid)
if singular_form_element == "ogranizations": if singular_form_element == "ogranizations":
return "org_id" return "org_id"

View file

@ -559,7 +559,6 @@ async def rbac_check(
res = await authorization_verify_if_element_is_public( res = await authorization_verify_if_element_is_public(
request, course_uuid, action, db_session request, course_uuid, action, db_session
) )
print("res", res)
return res return res
else: else:
res = await authorization_verify_based_on_roles_and_authorship_and_usergroups( res = await authorization_verify_based_on_roles_and_authorship_and_usergroups(

View file

@ -1,6 +1,12 @@
import stat
from typing import Literal from typing import Literal
from uuid import uuid4 from uuid import uuid4
from sqlmodel import Session, select from regex import R
from sqlalchemy import exists, union
from sqlmodel import Session, select, and_, or_
from src.db.usergroups import UserGroup
from src.db.usergroup_resources import UserGroupResource
from src.db.usergroup_user import UserGroupUser
from src.db.organizations import Organization from src.db.organizations import Organization
from src.db.trails import TrailRead from src.db.trails import TrailRead
from src.services.trail.trail import get_user_trail_with_orgid from src.services.trail.trail import get_user_trail_with_orgid
@ -105,7 +111,6 @@ async def get_course_meta(
) )
trail = TrailRead.from_orm(trail) trail = TrailRead.from_orm(trail)
return FullCourseReadWithTrail( return FullCourseReadWithTrail(
**course.dict(), **course.dict(),
chapters=chapters, chapters=chapters,
@ -326,26 +331,47 @@ async def get_courses_orgslug(
page: int = 1, page: int = 1,
limit: int = 10, limit: int = 10,
): ):
# Query for public courses
statement_public = ( statement_public = (
select(Course) select(Course)
.join(Organization) .join(Organization)
.where(Organization.slug == org_slug, Course.public == True) .where(Organization.slug == org_slug, Course.public == True)
) )
statement_all = (
select(Course).join(Organization).where(Organization.slug == org_slug) # Query for courses where the current user is an author
statement_author = (
select(Course)
.join(Organization)
.join(ResourceAuthor, ResourceAuthor.user_id == current_user.id)
.where(
Organization.slug == org_slug,
ResourceAuthor.resource_uuid == Course.course_uuid,
)
) )
if current_user.id == 0: # Query for courses where the current user is in a user group that has access to the course
statement = statement_public statement_usergroup = (
else: select(Course)
# RBAC check .join(Organization)
await authorization_verify_if_user_is_anon(current_user.id) .join(
UserGroupResource, UserGroupResource.resource_uuid == Course.course_uuid
)
.join(
UserGroupUser, UserGroupUser.usergroup_id == UserGroupResource.usergroup_id
)
.where(Organization.slug == org_slug, UserGroupUser.user_id == current_user.id)
)
statement = statement_all # Combine the results
statement_complete = union(
statement_public, statement_author, statement_usergroup
).subquery()
courses = db_session.exec(statement) courses = db_session.execute(select([statement_complete])).all()
courses = [CourseRead(**course.dict(), authors=[]) for course in courses] # TODO: I have no idea why this is necessary, but it is
courses = [CourseRead(**dict(course._mapping), authors=[]) for course in courses]
# for every course, get the authors # for every course, get the authors
for course in courses: for course in courses:
@ -366,6 +392,7 @@ async def get_courses_orgslug(
## 🔒 RBAC Utils ## ## 🔒 RBAC Utils ##
async def rbac_check( async def rbac_check(
request: Request, request: Request,
course_uuid: str, course_uuid: str,
@ -380,9 +407,11 @@ async def rbac_check(
) )
return res return res
else: else:
res = await authorization_verify_based_on_roles_and_authorship_and_usergroups( res = (
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
request, current_user.id, action, course_uuid, db_session request, current_user.id, action, course_uuid, db_session
) )
)
return res return res
else: else:
await authorization_verify_if_user_is_anon(current_user.id) await authorization_verify_if_user_is_anon(current_user.id)

View file

@ -135,6 +135,12 @@ async def install_default_elements(db_session: Session):
action_update=True, action_update=True,
action_delete=True, action_delete=True,
), ),
usergroups=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
collections=Permission( collections=Permission(
action_create=True, action_create=True,
action_read=True, action_read=True,
@ -183,6 +189,12 @@ async def install_default_elements(db_session: Session):
action_update=True, action_update=True,
action_delete=True, action_delete=True,
), ),
usergroups=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
collections=Permission( collections=Permission(
action_create=True, action_create=True,
action_read=True, action_read=True,
@ -231,6 +243,12 @@ async def install_default_elements(db_session: Session):
action_update=False, action_update=False,
action_delete=False, action_delete=False,
), ),
usergroups=Permission(
action_create=False,
action_read=True,
action_update=False,
action_delete=False,
),
collections=Permission( collections=Permission(
action_create=False, action_create=False,
action_read=True, action_read=True,

View file

@ -1,9 +1,15 @@
from datetime import datetime from datetime import datetime
import logging import logging
from typing import Literal
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.usergroup_ressources import UserGroupRessource from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship_and_usergroups,
authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon,
)
from src.db.usergroup_resources import UserGroupResource
from src.db.usergroup_user import UserGroupUser from src.db.usergroup_user import UserGroupUser
from src.db.organizations import Organization from src.db.organizations import Organization
from src.db.usergroups import UserGroup, UserGroupCreate, UserGroupRead, UserGroupUpdate from src.db.usergroups import UserGroup, UserGroupCreate, UserGroupRead, UserGroupUpdate
@ -19,6 +25,15 @@ async def create_usergroup(
usergroup = UserGroup.from_orm(usergroup_create) usergroup = UserGroup.from_orm(usergroup_create)
# RBAC check
await rbac_check(
request,
usergroup_uuid="usergroup_X",
current_user=current_user,
action="create",
db_session=db_session,
)
# Check if Organization exists # Check if Organization exists
statement = select(Organization).where(Organization.id == usergroup_create.org_id) statement = select(Organization).where(Organization.id == usergroup_create.org_id)
result = db_session.exec(statement) result = db_session.exec(statement)
@ -60,11 +75,50 @@ async def read_usergroup_by_id(
detail="UserGroup not found", detail="UserGroup not found",
) )
# RBAC check
await rbac_check(
request,
usergroup_uuid=usergroup.usergroup_uuid,
current_user=current_user,
action="read",
db_session=db_session,
)
usergroup = UserGroupRead.from_orm(usergroup) usergroup = UserGroupRead.from_orm(usergroup)
return usergroup return usergroup
async def read_usergroups_by_org_id(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
org_id: int,
) -> list[UserGroupRead]:
statement = select(UserGroup).where(UserGroup.org_id == org_id)
usergroups = db_session.exec(statement).all()
if not usergroups:
raise HTTPException(
status_code=404,
detail="UserGroups not found",
)
# RBAC check
await rbac_check(
request,
usergroup_uuid="usergroup_X",
current_user=current_user,
action="read",
db_session=db_session,
)
usergroups = [UserGroupRead.from_orm(usergroup) for usergroup in usergroups]
return usergroups
async def update_usergroup_by_id( async def update_usergroup_by_id(
request: Request, request: Request,
db_session: Session, db_session: Session,
@ -82,6 +136,15 @@ async def update_usergroup_by_id(
detail="UserGroup not found", detail="UserGroup not found",
) )
# RBAC check
await rbac_check(
request,
usergroup_uuid=usergroup.usergroup_uuid,
current_user=current_user,
action="update",
db_session=db_session,
)
usergroup.name = usergroup_update.name usergroup.name = usergroup_update.name
usergroup.description = usergroup_update.description usergroup.description = usergroup_update.description
usergroup.update_date = str(datetime.now()) usergroup.update_date = str(datetime.now())
@ -111,6 +174,15 @@ async def delete_usergroup_by_id(
detail="UserGroup not found", detail="UserGroup not found",
) )
# RBAC check
await rbac_check(
request,
usergroup_uuid=usergroup.usergroup_uuid,
current_user=current_user,
action="delete",
db_session=db_session,
)
db_session.delete(usergroup) db_session.delete(usergroup)
db_session.commit() db_session.commit()
@ -134,6 +206,15 @@ async def add_users_to_usergroup(
detail="UserGroup not found", detail="UserGroup not found",
) )
# RBAC check
await rbac_check(
request,
usergroup_uuid=usergroup.usergroup_uuid,
current_user=current_user,
action="create",
db_session=db_session,
)
user_ids_array = user_ids.split(",") user_ids_array = user_ids.split(",")
for user_id in user_ids_array: for user_id in user_ids_array:
@ -177,6 +258,16 @@ async def remove_users_from_usergroup(
detail="UserGroup not found", detail="UserGroup not found",
) )
# RBAC check
# RBAC check
await rbac_check(
request,
usergroup_uuid=usergroup.usergroup_uuid,
current_user=current_user,
action="delete",
db_session=db_session,
)
user_ids_array = user_ids.split(",") user_ids_array = user_ids.split(",")
for user_id in user_ids_array: for user_id in user_ids_array:
@ -192,12 +283,12 @@ async def remove_users_from_usergroup(
return "Users removed from UserGroup successfully" return "Users removed from UserGroup successfully"
async def add_ressources_to_usergroup( async def add_resources_to_usergroup(
request: Request, request: Request,
db_session: Session, db_session: Session,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
usergroup_id: int, usergroup_id: int,
ressources_uuids: str, resources_uuids: str,
) -> str: ) -> str:
statement = select(UserGroup).where(UserGroup.id == usergroup_id) statement = select(UserGroup).where(UserGroup.id == usergroup_id)
@ -209,14 +300,23 @@ async def add_ressources_to_usergroup(
detail="UserGroup not found", detail="UserGroup not found",
) )
ressources_uuids_array = ressources_uuids.split(",") # RBAC check
await rbac_check(
request,
usergroup_uuid=usergroup.usergroup_uuid,
current_user=current_user,
action="create",
db_session=db_session,
)
for ressource_uuid in ressources_uuids_array: resources_uuids_array = resources_uuids.split(",")
# TODO : Find a way to check if ressource exists
usergroup_obj = UserGroupRessource( for resource_uuid in resources_uuids_array:
# TODO : Find a way to check if resource exists
usergroup_obj = UserGroupResource(
usergroup_id=usergroup_id, usergroup_id=usergroup_id,
ressource_uuid=ressource_uuid, resource_uuid=resource_uuid,
org_id=usergroup.org_id, org_id=usergroup.org_id,
creation_date=str(datetime.now()), creation_date=str(datetime.now()),
update_date=str(datetime.now()), update_date=str(datetime.now()),
@ -226,15 +326,15 @@ async def add_ressources_to_usergroup(
db_session.commit() db_session.commit()
db_session.refresh(usergroup_obj) db_session.refresh(usergroup_obj)
return "Ressources added to UserGroup successfully" return "Resources added to UserGroup successfully"
async def remove_ressources_from_usergroup( async def remove_resources_from_usergroup(
request: Request, request: Request,
db_session: Session, db_session: Session,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
usergroup_id: int, usergroup_id: int,
ressources_uuids: str, resources_uuids: str,
) -> str: ) -> str:
statement = select(UserGroup).where(UserGroup.id == usergroup_id) statement = select(UserGroup).where(UserGroup.id == usergroup_id)
@ -246,20 +346,51 @@ async def remove_ressources_from_usergroup(
detail="UserGroup not found", detail="UserGroup not found",
) )
ressources_uuids_array = ressources_uuids.split(",") # RBAC check
await rbac_check(
for ressource_uuid in ressources_uuids_array: request,
statement = select(UserGroupRessource).where( usergroup_uuid=usergroup.usergroup_uuid,
UserGroupRessource.ressource_uuid == ressource_uuid current_user=current_user,
action="delete",
db_session=db_session,
) )
usergroup_ressource = db_session.exec(statement).first()
if usergroup_ressource: resources_uuids_array = resources_uuids.split(",")
db_session.delete(usergroup_ressource)
for resource_uuid in resources_uuids_array:
statement = select(UserGroupResource).where(
UserGroupResource.resource_uuid == resource_uuid
)
usergroup_resource = db_session.exec(statement).first()
if usergroup_resource:
db_session.delete(usergroup_resource)
db_session.commit() db_session.commit()
else: else:
logging.error( logging.error(f"resource with uuid {resource_uuid} not found in UserGroup")
f"Ressource with uuid {ressource_uuid} not found in UserGroup"
return "Resources removed from UserGroup successfully"
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
usergroup_uuid: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
request,
current_user.id,
action,
usergroup_uuid,
db_session,
) )
return "Ressources removed from UserGroup successfully"
## 🔒 RBAC Utils ##

View file

@ -453,7 +453,7 @@ async def authorize_user_action(
request: Request, request: Request,
db_session: Session, db_session: Session,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
ressource_uuid: str, resource_uuid: str,
action: Literal["create", "read", "update", "delete"], action: Literal["create", "read", "update", "delete"],
): ):
# Get user # Get user
@ -468,7 +468,7 @@ async def authorize_user_action(
# RBAC check # RBAC check
authorized = await authorization_verify_based_on_roles_and_authorship_and_usergroups( authorized = await authorization_verify_based_on_roles_and_authorship_and_usergroups(
request, current_user.id, action, ressource_uuid, db_session request, current_user.id, action, resource_uuid, db_session
) )
if authorized: if authorized: