diff --git a/apps/api/src/security/rbac/rbac.py b/apps/api/src/security/rbac/rbac.py index 04b64ca1..66a2dc89 100644 --- a/apps/api/src/security/rbac/rbac.py +++ b/apps/api/src/security/rbac/rbac.py @@ -142,7 +142,7 @@ async def authorization_verify_based_on_org_admin_status( # Tested and working -async def authorization_verify_based_on_roles_and_authorship_and_usergroups( +async def authorization_verify_based_on_roles_and_authorship( request: Request, user_id: int, action: Literal["read", "update", "delete", "create"], diff --git a/apps/api/src/services/courses/activities/activities.py b/apps/api/src/services/courses/activities/activities.py index 8b5c7d03..25591550 100644 --- a/apps/api/src/services/courses/activities/activities.py +++ b/apps/api/src/services/courses/activities/activities.py @@ -3,7 +3,7 @@ from sqlmodel import Session, select from src.db.courses.courses import Course from src.db.courses.chapters import Chapter from src.security.rbac.rbac import ( - authorization_verify_based_on_roles_and_authorship_and_usergroups, + authorization_verify_based_on_roles_and_authorship, authorization_verify_if_element_is_public, authorization_verify_if_user_is_anon, ) @@ -270,14 +270,14 @@ async def rbac_check( ) return res else: - res = await authorization_verify_based_on_roles_and_authorship_and_usergroups( + res = await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, course_uuid, db_session ) return res else: await authorization_verify_if_user_is_anon(current_user.id) - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py index 99b59b26..ae563e08 100644 --- a/apps/api/src/services/courses/activities/assignments.py +++ b/apps/api/src/services/courses/activities/assignments.py @@ -34,7 +34,7 @@ from src.security.features_utils.usage import ( increase_feature_usage, ) from src.security.rbac.rbac import ( - authorization_verify_based_on_roles_and_authorship_and_usergroups, + authorization_verify_based_on_roles_and_authorship, authorization_verify_if_element_is_public, authorization_verify_if_user_is_anon, ) @@ -1666,7 +1666,7 @@ async def rbac_check( return res else: res = ( - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, course_uuid, db_session ) ) @@ -1674,7 +1674,7 @@ async def rbac_check( else: await authorization_verify_if_user_is_anon(current_user.id) - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, diff --git a/apps/api/src/services/courses/activities/pdf.py b/apps/api/src/services/courses/activities/pdf.py index 30b4db9d..e6f2ca51 100644 --- a/apps/api/src/services/courses/activities/pdf.py +++ b/apps/api/src/services/courses/activities/pdf.py @@ -3,7 +3,7 @@ from src.db.courses.courses import Course from src.db.organizations import Organization from sqlmodel import Session, select from src.security.rbac.rbac import ( - authorization_verify_based_on_roles_and_authorship_and_usergroups, + authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon, ) from src.db.courses.chapters import Chapter @@ -150,7 +150,7 @@ async def rbac_check( ): await authorization_verify_if_user_is_anon(current_user.id) - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, diff --git a/apps/api/src/services/courses/activities/video.py b/apps/api/src/services/courses/activities/video.py index 3396607c..da428865 100644 --- a/apps/api/src/services/courses/activities/video.py +++ b/apps/api/src/services/courses/activities/video.py @@ -5,7 +5,7 @@ from src.db.organizations import Organization from pydantic import BaseModel from sqlmodel import Session, select from src.security.rbac.rbac import ( - authorization_verify_based_on_roles_and_authorship_and_usergroups, + authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon, ) from src.db.courses.chapters import Chapter @@ -232,7 +232,7 @@ async def rbac_check( ): await authorization_verify_if_user_is_anon(current_user.id) - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index fd34d842..5bc3e9bb 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -4,7 +4,7 @@ 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_and_usergroups, + authorization_verify_based_on_roles_and_authorship, authorization_verify_if_element_is_public, authorization_verify_if_user_is_anon, ) @@ -561,14 +561,14 @@ async def rbac_check( ) return res else: - res = await authorization_verify_based_on_roles_and_authorship_and_usergroups( + res = await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, course_uuid, db_session ) return res else: await authorization_verify_if_user_is_anon(current_user.id) - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, diff --git a/apps/api/src/services/courses/collections.py b/apps/api/src/services/courses/collections.py index 9c5f8412..d54faedf 100644 --- a/apps/api/src/services/courses/collections.py +++ b/apps/api/src/services/courses/collections.py @@ -4,7 +4,7 @@ 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_and_usergroups, + authorization_verify_based_on_roles_and_authorship, authorization_verify_if_element_is_public, authorization_verify_if_user_is_anon, ) @@ -300,7 +300,7 @@ async def rbac_check( ) else: res = ( - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, collection_uuid, db_session ) ) @@ -308,7 +308,7 @@ async def rbac_check( else: await authorization_verify_if_user_is_anon(current_user.id) - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index fa140944..2233d9c9 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -1,7 +1,6 @@ -from typing import Literal +from typing import Literal, List from uuid import uuid4 -from sqlalchemy import union -from sqlmodel import Session, select +from sqlmodel import Session, select, or_, and_ from src.db.usergroup_resources import UserGroupResource from src.db.usergroup_user import UserGroupUser from src.db.organizations import Organization @@ -21,7 +20,7 @@ from src.db.courses.courses import ( FullCourseReadWithTrail, ) from src.security.rbac.rbac import ( - authorization_verify_based_on_roles_and_authorship_and_usergroups, + authorization_verify_based_on_roles_and_authorship, authorization_verify_if_element_is_public, authorization_verify_if_user_is_anon, ) @@ -151,6 +150,69 @@ async def get_course_meta( trail=trail if trail else None, ) +async def get_courses_orgslug( + request: Request, + current_user: PublicUser | AnonymousUser, + org_slug: str, + db_session: Session, + page: int = 1, + limit: int = 10, +) -> List[CourseRead]: + offset = (page - 1) * limit + + # Base query + query = ( + select(Course) + .join(Organization) + .where(Organization.slug == org_slug) + ) + + if isinstance(current_user, AnonymousUser): + # For anonymous users, only show public courses + query = query.where(Course.public == True) + else: + # For authenticated users, show: + # 1. Public courses + # 2. Courses not in any UserGroup + # 3. Courses in UserGroups where the user is a member + # 4. Courses where the user is a resource author + query = ( + query + .outerjoin(UserGroupResource, UserGroupResource.resource_uuid == Course.course_uuid) # type: ignore + .outerjoin(UserGroupUser, and_( + UserGroupUser.usergroup_id == UserGroupResource.usergroup_id, + UserGroupUser.user_id == current_user.id + )) + .outerjoin(ResourceAuthor, ResourceAuthor.resource_uuid == Course.course_uuid) # type: ignore + .where(or_( + Course.public == True, + UserGroupResource.resource_uuid == None, # Courses not in any UserGroup # noqa: E711 + UserGroupUser.user_id == current_user.id, # Courses in UserGroups where user is a member + ResourceAuthor.user_id == current_user.id # Courses where user is a resource author + )) + ) + + # Apply pagination + query = query.offset(offset).limit(limit).distinct() + + courses = db_session.exec(query).all() + + # Fetch authors for each course + course_reads = [] + for course in courses: + authors_query = ( + select(User) + .join(ResourceAuthor, ResourceAuthor.user_id == User.id) # type: ignore + .where(ResourceAuthor.resource_uuid == course.course_uuid) + ) + authors = db_session.exec(authors_query).all() + + course_read = CourseRead.model_validate(course) + course_read.authors = [UserRead.model_validate(author) for author in authors] + course_reads.append(course_read) + + return course_reads + async def create_course( request: Request, @@ -366,72 +428,7 @@ async def delete_course( return {"detail": "Course deleted"} -async def get_courses_orgslug( - request: Request, - current_user: PublicUser | AnonymousUser, - org_slug: str, - db_session: Session, - page: int = 1, - limit: int = 10, -): - # TODO : This entire function is a mess. It needs to be rewritten. - - # Query for public courses - statement_public = ( - select(Course) - .join(Organization) - .where(Organization.slug == org_slug, Course.public == True) - ) - - # Query for courses where the current user is an author - statement_author = ( - select(Course) - .join(Organization) - .join(ResourceAuthor, ResourceAuthor.user_id == current_user.id) # type: ignore - .where( - Organization.slug == org_slug, - ResourceAuthor.resource_uuid == Course.course_uuid, - ) - ) - - # Query for courses where the current user is in a user group that has access to the course - statement_usergroup = ( - select(Course) - .join(Organization) - .join(UserGroupResource, UserGroupResource.resource_uuid == Course.course_uuid) # type: ignore - .join( - UserGroupUser, UserGroupUser.usergroup_id == UserGroupResource.usergroup_id # type: ignore - ) - .where(Organization.slug == org_slug, UserGroupUser.user_id == current_user.id) - ) - - # Combine the results - statement_complete = union( - statement_public, statement_author, statement_usergroup - ).subquery() - - # TODO: migrate this to exec - courses = db_session.execute(select(statement_complete)).all() # type: ignore - - # TODO: I have no idea why this is necessary, but it is - courses = [CourseRead(**course._asdict(), authors=[]) for course in courses] - - # for every course, get the authors - for course in courses: - authors_statement = ( - select(User) - .join(ResourceAuthor) - .where(ResourceAuthor.resource_uuid == course.course_uuid) - ) - authors = db_session.exec(authors_statement).all() - - # convert from User to UserRead - authors = [UserRead.model_validate(author) for author in authors] - - course.authors = authors - - return courses ## 🔒 RBAC Utils ## @@ -452,7 +449,7 @@ async def rbac_check( return res else: res = ( - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, course_uuid, db_session ) ) @@ -460,7 +457,7 @@ async def rbac_check( else: await authorization_verify_if_user_is_anon(current_user.id) - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, diff --git a/apps/api/src/services/roles/roles.py b/apps/api/src/services/roles/roles.py index c5939c2c..ea5d0715 100644 --- a/apps/api/src/services/roles/roles.py +++ b/apps/api/src/services/roles/roles.py @@ -2,7 +2,7 @@ from typing import Literal from uuid import uuid4 from sqlmodel import Session, select from src.security.rbac.rbac import ( - authorization_verify_based_on_roles_and_authorship_and_usergroups, + authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon, ) from src.db.users import AnonymousUser, PublicUser @@ -133,7 +133,7 @@ async def rbac_check( ): await authorization_verify_if_user_is_anon(current_user.id) - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, role_uuid, db_session ) diff --git a/apps/api/src/services/users/usergroups.py b/apps/api/src/services/users/usergroups.py index f443b6b2..acc68ba3 100644 --- a/apps/api/src/services/users/usergroups.py +++ b/apps/api/src/services/users/usergroups.py @@ -9,7 +9,7 @@ from src.security.features_utils.usage import ( increase_feature_usage, ) from src.security.rbac.rbac import ( - authorization_verify_based_on_roles_and_authorship_and_usergroups, + authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon, ) from src.db.usergroup_resources import UserGroupResource @@ -492,7 +492,7 @@ async def rbac_check( ): await authorization_verify_if_user_is_anon(current_user.id) - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index c7277122..401f48ab 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -15,7 +15,7 @@ from src.services.orgs.invites import get_invite_code from src.services.users.avatars import upload_avatar from src.db.roles import Role, RoleRead from src.security.rbac.rbac import ( - authorization_verify_based_on_roles_and_authorship_and_usergroups, + authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon, ) from src.db.organizations import Organization, OrganizationRead @@ -491,7 +491,7 @@ async def authorize_user_action( # RBAC check authorized = ( - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, resource_uuid, db_session ) ) @@ -564,7 +564,7 @@ async def rbac_check( if current_user.id == 0: # if user is anonymous return True else: - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, "create", "user_x", db_session ) @@ -575,7 +575,7 @@ async def rbac_check( if current_user.user_uuid == user_uuid: return True - await authorization_verify_based_on_roles_and_authorship_and_usergroups( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, action, user_uuid, db_session ) diff --git a/apps/web/app/auth/signup/InviteOnlySignUp.tsx b/apps/web/app/auth/signup/InviteOnlySignUp.tsx index 0ffa7952..c7a8bfc0 100644 --- a/apps/web/app/auth/signup/InviteOnlySignUp.tsx +++ b/apps/web/app/auth/signup/InviteOnlySignUp.tsx @@ -110,8 +110,10 @@ function InviteOnlySignUpComponent(props: InviteOnlySignUpProps) {
{message}

- -
Login
+ +
Login to your account
)}