feat: implement authorization with roles

This commit is contained in:
swve 2023-11-28 20:25:14 +01:00
parent 0595bfdb3f
commit 7738316200
19 changed files with 596 additions and 170 deletions

View file

@ -1,8 +1,13 @@
from typing import Literal
from sqlmodel import Session, select
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.organizations import Organization
from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate
from src.db.chapter_activities import ChapterActivity
from src.db.users import PublicUser
from src.db.users import AnonymousUser, PublicUser
from fastapi import HTTPException, Request
from uuid import uuid4
from datetime import datetime
@ -16,7 +21,7 @@ from datetime import datetime
async def create_activity(
request: Request,
activity_object: ActivityCreate,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
activity = Activity.from_orm(activity_object)
@ -31,6 +36,9 @@ async def create_activity(
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "create", db_session)
activity.activity_uuid = str(f"activity_{uuid4()}")
activity.creation_date = str(datetime.now())
activity.update_date = str(datetime.now())
@ -85,13 +93,16 @@ async def get_activity(
detail="Activity not found",
)
# RBAC check
await rbac_check(request, activity.activity_uuid, current_user, "read", db_session)
return activity
async def update_activity(
request: Request,
activity_object: ActivityUpdate,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Activity).where(Activity.id == activity_object.activity_id)
@ -103,6 +114,11 @@ async def update_activity(
detail="Activity not found",
)
# RBAC check
await rbac_check(
request, activity.activity_uuid, current_user, "update", db_session
)
del activity_object.activity_id
# Update only the fields that were passed in
@ -120,7 +136,7 @@ async def update_activity(
async def delete_activity(
request: Request,
activity_id: str,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Activity).where(Activity.id == activity_id)
@ -132,6 +148,11 @@ async def delete_activity(
detail="Activity not found",
)
# RBAC check
await rbac_check(
request, activity.activity_uuid, current_user, "delete", db_session
)
# Delete activity from chapter
statement = select(ChapterActivity).where(
ChapterActivity.activity_id == activity_id
@ -159,7 +180,7 @@ async def delete_activity(
async def get_activities(
request: Request,
coursechapter_id: str,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(ChapterActivity).where(
@ -173,4 +194,31 @@ async def get_activities(
detail="No activities found",
)
# RBAC check
await rbac_check(request, "activity_x", current_user, "read", db_session)
return activities
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
course_id: 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(
request,
current_user.id,
action,
course_id,
db_session,
)
## 🔒 RBAC Utils ##

View file

@ -1,4 +1,9 @@
from typing import Literal
from sqlmodel import Session, select
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.chapters import Chapter
from src.db.activities import (
Activity,
@ -8,7 +13,7 @@ from src.db.activities import (
)
from src.db.chapter_activities import ChapterActivity
from src.db.course_chapters import CourseChapter
from src.db.users import PublicUser
from src.db.users import AnonymousUser, PublicUser
from src.services.courses.activities.uploads.pdfs import upload_pdf
from fastapi import HTTPException, status, UploadFile, Request
from uuid import uuid4
@ -19,10 +24,13 @@ async def create_documentpdf_activity(
request: Request,
name: str,
chapter_id: str,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
pdf_file: UploadFile | None = None,
):
# RBAC check
await rbac_check(request, "activity_x", current_user, "create", db_session)
# get chapter_id
statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first()
@ -94,7 +102,7 @@ async def create_documentpdf_activity(
# Add activity to chapter
activity_chapter = ChapterActivity(
chapter_id=(int(chapter_id)),
activity_id=activity.id is not None,
activity_id=activity.id, # type: ignore
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
creation_date=str(datetime.now()),
@ -113,3 +121,27 @@ async def create_documentpdf_activity(
db_session.refresh(activity_chapter)
return ActivityRead.from_orm(activity)
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
course_id: 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(
request,
current_user.id,
action,
course_id,
db_session,
)
## 🔒 RBAC Utils ##

View file

@ -2,11 +2,20 @@ from typing import Literal
from pydantic import BaseModel
from sqlmodel import Session, select
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.chapters import Chapter
from src.db.activities import Activity, ActivityRead, ActivitySubTypeEnum, ActivityTypeEnum
from src.db.activities import (
Activity,
ActivityRead,
ActivitySubTypeEnum,
ActivityTypeEnum,
)
from src.db.chapter_activities import ChapterActivity
from src.db.course_chapters import CourseChapter
from src.db.users import PublicUser
from src.db.users import AnonymousUser, PublicUser
from src.services.courses.activities.uploads.videos import upload_video
from fastapi import HTTPException, status, UploadFile, Request
from uuid import uuid4
@ -21,6 +30,9 @@ async def create_video_activity(
db_session: Session,
video_file: UploadFile | None = None,
):
# RBAC check
await rbac_check(request, "activity_x", current_user, "create", db_session)
# get chapter_id
statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first()
@ -95,8 +107,8 @@ async def create_video_activity(
# update chapter
chapter_activity_object = ChapterActivity(
chapter_id=coursechapter.id is not None,
activity_id=activity.id is not None,
chapter_id=chapter.id, # type: ignore
activity_id=activity.id, # type: ignore
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
creation_date=str(datetime.now()),
@ -111,6 +123,7 @@ async def create_video_activity(
return ActivityRead.from_orm(activity)
class ExternalVideo(BaseModel):
name: str
uri: str
@ -124,10 +137,13 @@ class ExternalVideoInDB(BaseModel):
async def create_external_video_activity(
request: Request,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
data: ExternalVideo,
db_session: Session,
):
# RBAC check
await rbac_check(request, "activity_x", current_user, "create", db_session)
# get chapter_id
statement = select(Chapter).where(Chapter.id == data.chapter_id)
chapter = db_session.exec(statement).first()
@ -174,8 +190,8 @@ async def create_external_video_activity(
# update chapter
chapter_activity_object = ChapterActivity(
chapter_id=coursechapter.id is not None,
activity_id=activity.id is not None,
chapter_id=coursechapter.id, # type: ignore
activity_id=activity.id, # type: ignore
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=1,
@ -186,3 +202,24 @@ async def create_external_video_activity(
db_session.commit()
return ActivityRead.from_orm(activity)
async def rbac_check(
request: Request,
course_id: 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(
request,
current_user.id,
action,
course_id,
db_session,
)
## 🔒 RBAC Utils ##

View file

@ -1,7 +1,12 @@
from datetime import datetime
from typing import List
from typing import List, Literal
from uuid import uuid4
from sqlmodel import Session, select
from src.db.users import AnonymousUser
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.course_chapters import CourseChapter
from src.db.activities import Activity, ActivityRead
from src.db.chapter_activities import ChapterActivity
@ -26,11 +31,14 @@ from fastapi import HTTPException, status, Request
async def create_chapter(
request: Request,
chapter_object: ChapterCreate,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> ChapterRead:
chapter = Chapter.from_orm(chapter_object)
# RBAC check
await rbac_check(request, "chapter_x", current_user, "create", db_session)
# complete chapter object
chapter.course_id = chapter_object.course_id
chapter.chapter_uuid = f"chapter_{uuid4()}"
@ -87,7 +95,7 @@ async def create_chapter(
async def get_chapter(
request: Request,
chapter_id: int,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> ChapterRead:
statement = select(Chapter).where(Chapter.id == chapter_id)
@ -98,6 +106,9 @@ async def get_chapter(
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
)
# RBAC check
await rbac_check(request, chapter.chapter_uuid, current_user, "read", db_session)
# Get activities for this chapter
statement = (
select(Activity)
@ -119,7 +130,7 @@ async def get_chapter(
async def update_chapter(
request: Request,
chapter_object: ChapterUpdate,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> ChapterRead:
statement = select(Chapter).where(Chapter.id == chapter_object.chapter_id)
@ -130,6 +141,9 @@ async def update_chapter(
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
)
# RBAC check
await rbac_check(request, chapter.chapter_uuid, current_user, "update", db_session)
# Update only the fields that were passed in
for var, value in vars(chapter_object).items():
if value is not None:
@ -148,7 +162,7 @@ async def update_chapter(
async def delete_chapter(
request: Request,
chapter_id: str,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Chapter).where(Chapter.id == chapter_id)
@ -159,6 +173,9 @@ async def delete_chapter(
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
)
# RBAC check
await rbac_check(request, chapter.chapter_uuid, current_user, "delete", db_session)
db_session.delete(chapter)
db_session.commit()
@ -173,15 +190,12 @@ async def delete_chapter(
return {"detail": "chapter deleted"}
####################################################
# Misc
####################################################
async def get_course_chapters(
request: Request,
course_id: int,
db_session: Session,
current_user: PublicUser | AnonymousUser,
page: int = 1,
limit: int = 10,
) -> List[ChapterRead]:
@ -195,6 +209,9 @@ async def get_course_chapters(
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
# RBAC check
await rbac_check(request, "chapter_x", current_user, "read", db_session)
# Get activities for each chapter
for chapter in chapters:
statement = (
@ -233,6 +250,9 @@ async def get_depreceated_course_chapters(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
# Get chapters that are linked to his course and order them by order, using the order field in the CourseChapter table
statement = (
select(Chapter)
@ -310,6 +330,9 @@ async def reorder_chapters_and_activities(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
###########
# Chapters
###########
@ -469,3 +492,27 @@ async def reorder_chapters_and_activities(
db_session.commit()
return {"detail": "Chapters reordered"}
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
course_id: 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(
request,
current_user.id,
action,
course_id,
db_session,
)
## 🔒 RBAC Utils ##

View file

@ -1,7 +1,12 @@
from datetime import datetime
from typing import List
from typing import List, Literal
from uuid import uuid4
from sqlmodel import Session, select
from src.db.users import AnonymousUser
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.collections import (
Collection,
CollectionCreate,
@ -37,6 +42,11 @@ async def get_collection(
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
)
# RBAC check
await rbac_check(
request, collection.collection_uuid, current_user, "read", db_session
)
# get courses in collection
statement = (
select(Course)
@ -58,6 +68,9 @@ async def create_collection(
) -> CollectionRead:
collection = Collection.from_orm(collection_object)
# RBAC check
await rbac_check(request, "collection_x", current_user, "create", db_session)
# Complete the collection object
collection.collection_uuid = f"collection_{uuid4()}"
collection.creation_date = str(datetime.now())
@ -70,16 +83,17 @@ async def create_collection(
db_session.refresh(collection)
# Link courses to collection
for course in collection_object.courses:
collection_course = CollectionCourse(
collection_id=int(collection.id is not None),
course_id=int(course),
org_id=int(collection_object.org_id),
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
# Add collection_course to database
db_session.add(collection_course)
if collection:
for course_id in collection_object.courses:
collection_course = CollectionCourse(
collection_id=int(collection.id), # type: ignore
course_id=course_id,
org_id=int(collection_object.org_id),
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
# Add collection_course to database
db_session.add(collection_course)
db_session.commit()
db_session.refresh(collection)
@ -113,6 +127,11 @@ async def update_collection(
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
)
# RBAC check
await rbac_check(
request, collection.collection_uuid, current_user, "update", db_session
)
courses = collection_object.courses
del collection_object.collection_id
@ -142,7 +161,7 @@ async def update_collection(
# Add new collection_courses
for course in courses or []:
collection_course = CollectionCourse(
collection_id=int(collection.id is not None),
collection_id=int(collection.id), # type: ignore
course_id=int(course),
org_id=int(collection.org_id),
creation_date=str(datetime.now()),
@ -180,6 +199,11 @@ async def delete_collection(
detail="Collection not found",
)
# RBAC check
await rbac_check(
request, collection.collection_uuid, current_user, "delete", db_session
)
# delete collection from database
db_session.delete(collection)
db_session.commit()
@ -195,11 +219,14 @@ async def delete_collection(
async def get_collections(
request: Request,
org_id: str,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
page: int = 1,
limit: int = 10,
) -> List[CollectionRead]:
# RBAC check
await rbac_check(request, "collection_x", current_user, "read", db_session)
statement = (
select(Collection).where(Collection.org_id == org_id).distinct(Collection.id)
)
@ -223,3 +250,27 @@ async def get_collections(
collections_with_courses.append(collection)
return collections_with_courses
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
course_id: 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(
request,
current_user.id,
action,
course_id,
db_session,
)
## 🔒 RBAC Utils ##

View file

@ -1,12 +1,28 @@
from calendar import c
import json
from queue import Full
import resource
from typing import Literal
from uuid import uuid4
from sqlmodel import Session, select
from src.db import chapters
from src.db.activities import Activity, ActivityRead
from src.db.chapter_activities import ChapterActivity
from src.db.chapters import Chapter, ChapterRead
from src.db.organizations import Organization
from src.db.trails import TrailRead
from src.services.trail.trail import get_user_trail_with_orgid
from src import db
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.users import PublicUser, AnonymousUser
from src.db.courses import Course, CourseCreate, CourseRead, CourseUpdate
from src.db.courses import (
Course,
CourseCreate,
CourseRead,
CourseUpdate,
FullCourseReadWithTrail,
)
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_element_is_public,
@ -18,7 +34,10 @@ from datetime import datetime
async def get_course(
request: Request, course_id: str, current_user: PublicUser, db_session: Session
request: Request,
course_id: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Course).where(Course.id == course_id)
course = db_session.exec(statement).first()
@ -29,12 +48,21 @@ async def get_course(
detail="Course not found",
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
return course
async def get_course_meta(
request: Request, course_id: str, current_user: PublicUser, db_session: Session
):
request: Request,
course_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> FullCourseReadWithTrail:
# Avoid circular import
from src.services.courses.chapters import get_course_chapters
course_statement = select(Course).where(Course.id == course_id)
course = db_session.exec(course_statement).first()
@ -44,22 +72,40 @@ async def get_course_meta(
detail="Course not found",
)
# todo : get course chapters
# todo : get course activities
# todo : get trail
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
return course
course = CourseRead.from_orm(course)
# Get course chapters
chapters = await get_course_chapters(request, course.id, db_session, current_user)
# Trail
trail = await get_user_trail_with_orgid(
request, current_user, course.org_id, db_session
)
trail = TrailRead.from_orm(trail)
return FullCourseReadWithTrail(
**course.dict(),
chapters=chapters,
trail=trail,
)
async def create_course(
request: Request,
course_object: CourseCreate,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
thumbnail_file: UploadFile | None = None,
):
course = Course.from_orm(course_object)
# RBAC check
await rbac_check(request, "course_x", current_user, "create", db_session)
# Complete course object
course.org_id = course.org_id
course.course_uuid = str(f"course_{uuid4()}")
@ -69,7 +115,9 @@ async def create_course(
# Upload thumbnail
if thumbnail_file and thumbnail_file.filename:
name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
await upload_thumbnail(thumbnail_file, name_in_disk, course_object.org_id, course.course_uuid)
await upload_thumbnail(
thumbnail_file, name_in_disk, course_object.org_id, course.course_uuid
)
course_object.thumbnail = name_in_disk
# Insert course
@ -97,7 +145,7 @@ async def create_course(
async def update_course_thumbnail(
request: Request,
course_id: str,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
thumbnail_file: UploadFile | None = None,
):
@ -112,6 +160,9 @@ async def update_course_thumbnail(
detail="Course not found",
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
# Upload thumbnail
if thumbnail_file and thumbnail_file.filename:
name_in_disk = (
@ -143,7 +194,7 @@ async def update_course_thumbnail(
async def update_course(
request: Request,
course_object: CourseUpdate,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Course).where(Course.id == course_object.course_id)
@ -154,7 +205,10 @@ async def update_course(
status_code=404,
detail="Course not found",
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
del course_object.course_id
# Update only the fields that were passed in
@ -173,7 +227,10 @@ async def update_course(
async def delete_course(
request: Request, course_id: str, current_user: PublicUser, db_session: Session
request: Request,
course_id: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Course).where(Course.id == course_id)
course = db_session.exec(statement).first()
@ -184,92 +241,74 @@ async def delete_course(
detail="Course not found",
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
db_session.delete(course)
db_session.commit()
return {"detail": "Course deleted"}
####################################################
# Misc
####################################################
async def get_courses_orgslug(
request: Request,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
org_slug: str,
db_session: Session,
page: int = 1,
limit: int = 10,
org_slug: str | None = None,
):
courses = request.app.db["courses"]
orgs = request.app.db["organizations"]
statement_public = (
select(Course)
.join(Organization)
.where(Organization.slug == org_slug, Course.public == True)
)
statement_all = (
select(Course).join(Organization).where(Organization.slug == org_slug)
)
# get org_id from slug
org = await orgs.find_one({"slug": org_slug})
if not org:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
)
# show only public courses if user is not logged in
if current_user.id == "anonymous":
all_courses = (
courses.find({"org_id": org["org_id"], "public": True})
.sort("name", 1)
.skip(10 * (page - 1))
.limit(limit)
)
if current_user.id == 0:
statement = statement_public
else:
all_courses = (
courses.find({"org_id": org["org_id"]})
.sort("name", 1)
.skip(10 * (page - 1))
.limit(limit)
)
# RBAC check
await authorization_verify_if_user_is_anon(current_user.id)
return [
json.loads(json.dumps(course, default=str))
for course in await all_courses.to_list(length=100)
]
statement = statement_all
courses = db_session.exec(statement)
return courses
#### Security ####################################################
## 🔒 RBAC Utils ##
async def verify_rights(
async def rbac_check(
request: Request,
course_id: str,
course_uuid: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
if action == "read":
if current_user.id == "anonymous":
if current_user.id == 0: # Anonymous user
await authorization_verify_if_element_is_public(
request, course_id, str(current_user.id), action, db_session
request, course_uuid, action, db_session
)
else:
await authorization_verify_based_on_roles_and_authorship(
request,
str(current_user.id),
action,
course_id,
db_session,
request, current_user.id, action, course_uuid, db_session
)
else:
await authorization_verify_if_user_is_anon(str(current_user.id))
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship(
request,
str(current_user.id),
current_user.id,
action,
course_id,
course_uuid,
db_session,
)
#### Security ####################################################
## 🔒 RBAC Utils ##

View file

@ -1,7 +1,12 @@
from datetime import datetime
from typing import Literal
from uuid import uuid4
from sqlmodel import Session, select
from src.db.users import PublicUser
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.users import AnonymousUser, PublicUser
from src.db.user_organizations import UserOrganization
from src.db.organizations import (
Organization,
@ -13,7 +18,12 @@ from src.services.orgs.logos import upload_org_logo
from fastapi import HTTPException, UploadFile, status, Request
async def get_organization(request: Request, org_id: str, db_session: Session):
async def get_organization(
request: Request,
org_id: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
@ -25,11 +35,17 @@ async def get_organization(request: Request, org_id: str, db_session: Session):
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
return org
async def get_organization_by_slug(
request: Request, org_slug: str, db_session: Session
request: Request,
org_slug: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
statement = select(Organization).where(Organization.slug == org_slug)
result = db_session.exec(statement)
@ -42,13 +58,16 @@ async def get_organization_by_slug(
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
return org
async def create_org(
request: Request,
org_object: OrganizationCreate,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.slug == org_object.slug)
@ -64,6 +83,9 @@ async def create_org(
org = Organization.from_orm(org_object)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "create", db_session)
# Complete the org object
org.org_uuid = f"org_{uuid4()}"
org.creation_date = str(datetime.now())
@ -92,7 +114,7 @@ async def create_org(
async def update_org(
request: Request,
org_object: OrganizationUpdate,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.id == org_object.org_id)
@ -106,6 +128,9 @@ async def update_org(
detail="Organization slug not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
org = Organization.from_orm(org_object)
# Verify if the new slug is already in use
@ -142,7 +167,7 @@ async def update_org_logo(
request: Request,
logo_file: UploadFile,
org_id: str,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.id == org_id)
@ -156,6 +181,9 @@ async def update_org_logo(
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# Upload logo
name_in_disk = await upload_org_logo(logo_file, org_id)
@ -173,7 +201,10 @@ async def update_org_logo(
async def delete_org(
request: Request, org_id: str, current_user: PublicUser, db_session: Session
request: Request,
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
@ -186,6 +217,9 @@ async def delete_org(
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "delete", db_session)
db_session.delete(org)
db_session.commit()
@ -224,3 +258,28 @@ async def get_orgs_by_user(
orgs = result.all()
return orgs
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
org_id: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
# Organizations are readable by anyone
if action == "read":
return True
else:
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, org_id, db_session
)
## 🔒 RBAC Utils ##

View file

@ -1,6 +1,12 @@
from typing import Literal
from uuid import uuid4
from sqlmodel import Session, select
from src.db.users import PublicUser
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
authorization_verify_if_user_is_author,
)
from src.db.users import AnonymousUser, PublicUser
from src.db.roles import Role, RoleCreate, RoleUpdate
from fastapi import HTTPException, Request
from datetime import datetime
@ -14,6 +20,9 @@ async def create_role(
):
role = Role.from_orm(role_object)
# RBAC check
await rbac_check(request, current_user, "create", "role_xxx", db_session)
# Complete the role object
role.role_uuid = f"role_{uuid4()}"
role.creation_date = str(datetime.now())
@ -40,6 +49,9 @@ async def read_role(
detail="Role not found",
)
# RBAC check
await rbac_check(request, current_user, "read", role.role_uuid, db_session)
return role
@ -60,6 +72,9 @@ async def update_role(
detail="Role not found",
)
# RBAC check
await rbac_check(request, current_user, "update", role.role_uuid, db_session)
# Complete the role object
role.update_date = str(datetime.now())
@ -81,6 +96,9 @@ async def update_role(
async def delete_role(
request: Request, db_session: Session, role_id: str, current_user: PublicUser
):
# RBAC check
await rbac_check(request, current_user, "delete", role_id, db_session)
statement = select(Role).where(Role.id == role_id)
result = db_session.exec(statement)
@ -96,3 +114,23 @@ async def delete_role(
db_session.commit()
return "Role deleted"
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
role_uuid: str,
db_session: Session,
):
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, role_uuid, db_session
)
## 🔒 RBAC Utils ##

View file

@ -6,7 +6,7 @@ from src.db.courses import Course
from src.db.trail_runs import TrailRun, TrailRunRead
from src.db.trail_steps import TrailStep
from src.db.trails import Trail, TrailCreate, TrailRead
from src.db.users import PublicUser
from src.db.users import AnonymousUser, PublicUser
async def create_user_trail(
@ -80,7 +80,7 @@ async def get_user_trails(
async def get_user_trail_with_orgid(
request: Request, user: PublicUser, org_id: int, db_session: Session
request: Request, user: PublicUser | AnonymousUser, org_id: int, db_session: Session
) -> TrailRead:
statement = select(Trail).where(Trail.org_id == org_id, Trail.user_id == user.id)
trail = db_session.exec(statement).first()

View file

@ -1,9 +1,17 @@
from datetime import datetime
from typing import Literal
from uuid import uuid4
from fastapi import HTTPException, Request, status
from sqlmodel import Session, select
from src import db
from src.security.rbac.rbac import (
authorization_verify_based_on_roles,
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.organizations import Organization
from src.db.users import (
AnonymousUser,
PublicUser,
User,
UserCreate,
@ -18,12 +26,15 @@ from src.security.security import security_hash_password, security_verify_passwo
async def create_user(
request: Request,
db_session: Session,
current_user: PublicUser | None,
current_user: PublicUser | AnonymousUser,
user_object: UserCreate,
org_id: int,
):
user = User.from_orm(user_object)
# RBAC check
await rbac_check(request, current_user, "create", "user_x", db_session)
# Complete the user object
user.user_uuid = f"user_{uuid4()}"
user.password = await security_hash_password(user_object.password)
@ -94,11 +105,14 @@ async def create_user(
async def create_user_without_org(
request: Request,
db_session: Session,
current_user: PublicUser | None,
current_user: PublicUser | AnonymousUser,
user_object: UserCreate,
):
user = User.from_orm(user_object)
# RBAC check
await rbac_check(request, current_user, "create", "user_x", db_session)
# Complete the user object
user.user_uuid = f"user_{uuid4()}"
user.password = await security_hash_password(user_object.password)
@ -146,7 +160,7 @@ async def create_user_without_org(
async def update_user(
request: Request,
db_session: Session,
current_user: PublicUser | None,
current_user: PublicUser | AnonymousUser,
user_object: UserUpdate,
):
# Get user
@ -158,6 +172,9 @@ async def update_user(
status_code=400,
detail="User does not exist",
)
# RBAC check
await rbac_check(request, current_user, "update", user.user_uuid, db_session)
# Update user
user_data = user_object.dict(exclude_unset=True)
@ -179,7 +196,7 @@ async def update_user(
async def update_user_password(
request: Request,
db_session: Session,
current_user: PublicUser | None,
current_user: PublicUser | AnonymousUser,
form: UserUpdatePassword,
):
# Get user
@ -191,6 +208,9 @@ async def update_user_password(
status_code=400,
detail="User does not exist",
)
# RBAC check
await rbac_check(request, current_user, "update", user.user_uuid, db_session)
if not await security_verify_password(form.old_password, user.password):
raise HTTPException(
@ -214,7 +234,7 @@ async def update_user_password(
async def read_user_by_id(
request: Request,
db_session: Session,
current_user: PublicUser | None,
current_user: PublicUser | AnonymousUser,
user_id: int,
):
# Get user
@ -227,6 +247,9 @@ async def read_user_by_id(
detail="User does not exist",
)
# RBAC check
await rbac_check(request, current_user, "read", user.user_uuid, db_session)
user = UserRead.from_orm(user)
return user
@ -235,11 +258,11 @@ async def read_user_by_id(
async def read_user_by_uuid(
request: Request,
db_session: Session,
current_user: PublicUser | None,
uuid: str,
current_user: PublicUser | AnonymousUser,
user_uuid: str,
):
# Get user
statement = select(User).where(User.user_uuid == uuid)
statement = select(User).where(User.user_uuid == user_uuid)
user = db_session.exec(statement).first()
if not user:
@ -248,6 +271,9 @@ async def read_user_by_uuid(
detail="User does not exist",
)
# RBAC check
await rbac_check(request, current_user, "read", user.user_uuid, db_session)
user = UserRead.from_orm(user)
return user
@ -256,7 +282,7 @@ async def read_user_by_uuid(
async def delete_user_by_id(
request: Request,
db_session: Session,
current_user: PublicUser | None,
current_user: PublicUser | AnonymousUser,
user_id: int,
):
# Get user
@ -269,6 +295,9 @@ async def delete_user_by_id(
detail="User does not exist",
)
# RBAC check
await rbac_check(request, current_user, "delete", user.user_uuid, db_session)
# Delete user
db_session.delete(user)
db_session.commit()
@ -293,3 +322,37 @@ async def security_get_user(request: Request, db_session: Session, email: str) -
user = User(**user.dict())
return user
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
user_uuid: str,
db_session: Session,
):
if action == "create":
if current_user.id == 0: # if user is anonymous
return True
else:
res = await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, "create", "user_x", db_session
)
else:
await authorization_verify_if_user_is_anon(current_user.id)
# if user is the same as the one being read
if current_user.user_uuid == user_uuid:
return True
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, "read", action, db_session
)
## 🔒 RBAC Utils ##