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 ##