diff --git a/apps/api/app.py b/apps/api/app.py index aa3b4b2e..6b99c011 100644 --- a/apps/api/app.py +++ b/apps/api/app.py @@ -11,8 +11,6 @@ from fastapi_jwt_auth.exceptions import AuthJWTException from fastapi.middleware.gzip import GZipMiddleware -# from src.services.mocks.initial import create_initial_data - ######################## # Pre-Alpha Version 0.1.0 # Author: @swve diff --git a/apps/api/cli.py b/apps/api/cli.py index b4238ac4..5e3f8142 100644 --- a/apps/api/cli.py +++ b/apps/api/cli.py @@ -49,6 +49,8 @@ def install( email="", logo_image="", thumbnail_image="", + about="", + label="", ) install_create_organization(org, db_session) print("Default organization created ✅") @@ -91,6 +93,8 @@ def install( email="", logo_image="", thumbnail_image="", + about="", + label="", ) install_create_organization(org, db_session) print(orgname + " Organization created ✅") diff --git a/apps/api/src/db/roles.py b/apps/api/src/db/roles.py index 7d1c8a0b..7b6bd4e2 100644 --- a/apps/api/src/db/roles.py +++ b/apps/api/src/db/roles.py @@ -16,14 +16,36 @@ class Permission(BaseModel): return getattr(self, item) +class PermissionsWithOwn(BaseModel): + action_create: bool + action_read: bool + action_read_own: bool + action_update: bool + action_update_own: bool + action_delete: bool + action_delete_own: bool + + def __getitem__(self, item): + return getattr(self, item) + + +class DashboardPermission(BaseModel): + action_access: bool + + def __getitem__(self, item): + return getattr(self, item) + + class Rights(BaseModel): - courses: Permission + courses: PermissionsWithOwn users: Permission usergroups : Permission collections: Permission organizations: Permission coursechapters: Permission activities: Permission + roles: Permission + dashboard: DashboardPermission def __getitem__(self, item): return getattr(self, item) diff --git a/apps/api/src/security/rbac/rbac.py b/apps/api/src/security/rbac/rbac.py index 1e56238c..5bf64bba 100644 --- a/apps/api/src/security/rbac/rbac.py +++ b/apps/api/src/security/rbac/rbac.py @@ -7,7 +7,7 @@ from src.db.courses.courses import Course from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum from src.db.roles import Role from src.db.user_organizations import UserOrganization -from src.security.rbac.utils import check_element_type +from src.security.rbac.utils import check_element_type, check_course_permissions_with_own # Tested and working @@ -106,14 +106,30 @@ async def authorization_verify_based_on_roles( user_roles_in_organization_and_standard_roles = db_session.exec(statement).all() + + # Check if user is the author of the resource for "own" permissions + is_author = False + if action in ["update", "delete", "read"]: + is_author = await authorization_verify_if_user_is_author( + request, user_id, action, element_uuid, db_session + ) + # Check all roles until we find one that grants the permission for role in user_roles_in_organization_and_standard_roles: role = Role.model_validate(role) if role.rights: rights = role.rights element_rights = getattr(rights, element_type, None) - if element_rights and getattr(element_rights, f"action_{action}", False): - return True + if element_rights: + # Special handling for courses with PermissionsWithOwn + if element_type == "courses": + if await check_course_permissions_with_own(element_rights, action, is_author): + return True + else: + # For non-course resources, only check general permissions + # (regular Permission class no longer has "own" permissions) + if getattr(element_rights, f"action_{action}", False): + return True # If we get here, no role granted the permission return False diff --git a/apps/api/src/security/rbac/utils.py b/apps/api/src/security/rbac/utils.py index d6960a24..9904e65d 100644 --- a/apps/api/src/security/rbac/utils.py +++ b/apps/api/src/security/rbac/utils.py @@ -30,6 +30,38 @@ async def check_element_type(element_uuid): ) +async def check_course_permissions_with_own( + element_rights, + action: str, + is_author: bool = False +) -> bool: + """ + Check course-specific permissions including "own" permissions. + + Args: + element_rights: The rights object for courses (PermissionsWithOwn) + action: The action to check ("read", "update", "delete", "create") + is_author: Whether the user is the author of the course + + Returns: + bool: True if permission is granted, False otherwise + """ + if not element_rights: + return False + + # Check for general permission first + if getattr(element_rights, f"action_{action}", False): + return True + + # Check for "own" permission if user is the author + if is_author: + own_action = f"action_{action}_own" + if getattr(element_rights, own_action, False): + return True + + return False + + async def get_singular_form_of_element(element_uuid): element_type = await check_element_type(element_uuid) diff --git a/apps/api/src/services/courses/activities/pdf.py b/apps/api/src/services/courses/activities/pdf.py index e6f2ca51..b9fe563f 100644 --- a/apps/api/src/services/courses/activities/pdf.py +++ b/apps/api/src/services/courses/activities/pdf.py @@ -31,7 +31,7 @@ async def create_documentpdf_activity( pdf_file: UploadFile | None = None, ): # RBAC check - await rbac_check(request, "activity_x", current_user, "create", db_session) + await rbac_check(request, "course_uuid", current_user, "create", db_session) # get chapter_id statement = select(Chapter).where(Chapter.id == chapter_id) @@ -94,9 +94,7 @@ async def create_documentpdf_activity( content={ "filename": "documentpdf." + pdf_format, "activity_uuid": activity_uuid, - }, - published_version=1, - version=1, + }, org_id=org_id if org_id else 0, course_id=coursechapter.course_id, activity_uuid=activity_uuid, diff --git a/apps/api/src/services/courses/activities/video.py b/apps/api/src/services/courses/activities/video.py index 71408033..dc7d3ab5 100644 --- a/apps/api/src/services/courses/activities/video.py +++ b/apps/api/src/services/courses/activities/video.py @@ -99,13 +99,11 @@ async def create_video_activity( activity_uuid=activity_uuid, org_id=coursechapter.org_id, course_id=coursechapter.course_id, - published_version=1, content={ "filename": "video." + video_format, "activity_uuid": activity_uuid, }, details=details, - version=1, creation_date=str(datetime.now()), update_date=str(datetime.now()), ) @@ -198,14 +196,12 @@ async def create_external_video_activity( activity_uuid=activity_uuid, course_id=coursechapter.course_id, org_id=coursechapter.org_id, - published_version=1, content={ "uri": data.uri, "type": data.type, "activity_uuid": activity_uuid, }, details=details, - version=1, creation_date=str(datetime.now()), update_date=str(datetime.now()), ) diff --git a/apps/api/src/services/install/install.py b/apps/api/src/services/install/install.py index aea4551e..559967f3 100644 --- a/apps/api/src/services/install/install.py +++ b/apps/api/src/services/install/install.py @@ -24,7 +24,7 @@ from src.db.organization_config import ( UserGroupOrgConfig, ) from src.db.organizations import Organization, OrganizationCreate -from src.db.roles import Permission, Rights, Role, RoleTypeEnum +from src.db.roles import DashboardPermission, Permission, PermissionsWithOwn, Rights, Role, RoleTypeEnum from src.db.user_organizations import UserOrganization from src.db.users import User, UserCreate, UserRead from config.config import get_learnhouse_config @@ -127,7 +127,7 @@ def install_default_elements(db_session: Session): statement = select(Role).where(Role.role_type == RoleTypeEnum.TYPE_GLOBAL) roles = db_session.exec(statement).all() - if roles and len(roles) == 3: + if roles and len(roles) == 4: raise HTTPException( status_code=409, detail="Default roles already exist", @@ -136,16 +136,19 @@ def install_default_elements(db_session: Session): # Create default roles role_global_admin = Role( name="Admin", - description="Standard Admin Role", + description="Full platform control", id=1, role_type=RoleTypeEnum.TYPE_GLOBAL, role_uuid="role_global_admin", rights=Rights( - courses=Permission( + courses=PermissionsWithOwn( action_create=True, action_read=True, + action_read_own=True, action_update=True, + action_update_own=True, action_delete=True, + action_delete_own=True, ), users=Permission( action_create=True, @@ -183,6 +186,15 @@ def install_default_elements(db_session: Session): action_update=True, action_delete=True, ), + roles=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + dashboard=DashboardPermission( + action_access=True, + ), ), creation_date=str(datetime.now()), update_date=str(datetime.now()), @@ -190,22 +202,25 @@ def install_default_elements(db_session: Session): role_global_maintainer = Role( name="Maintainer", - description="Standard Maintainer Role", + description="Mid-level manager, wide permissions but no platform control", id=2, role_type=RoleTypeEnum.TYPE_GLOBAL, role_uuid="role_global_maintainer", rights=Rights( - courses=Permission( + courses=PermissionsWithOwn( action_create=True, action_read=True, + action_read_own=True, action_update=True, + action_update_own=True, action_delete=True, + action_delete_own=True, ), users=Permission( action_create=True, action_read=True, action_update=True, - action_delete=True, + action_delete=False, ), usergroups=Permission( action_create=True, @@ -220,10 +235,10 @@ def install_default_elements(db_session: Session): action_delete=True, ), organizations=Permission( - action_create=True, + action_create=False, action_read=True, - action_update=True, - action_delete=True, + action_update=False, + action_delete=False, ), coursechapters=Permission( action_create=True, @@ -237,6 +252,81 @@ def install_default_elements(db_session: Session): action_update=True, action_delete=True, ), + roles=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + dashboard=DashboardPermission( + action_access=True, + ), + ), + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + ) + + role_global_instructor = Role( + name="Instructor", + description="Can manage their own content", + id=3, + role_type=RoleTypeEnum.TYPE_GLOBAL, + role_uuid="role_global_instructor", + rights=Rights( + courses=PermissionsWithOwn( + action_create=True, + action_read=True, + action_read_own=True, + action_update=False, + action_update_own=True, + action_delete=False, + action_delete_own=True, + ), + users=Permission( + action_create=False, + action_read=False, + action_update=False, + action_delete=False, + ), + usergroups=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + collections=Permission( + action_create=True, + action_read=True, + action_update=False, + action_delete=False, + ), + organizations=Permission( + action_create=False, + action_read=False, + action_update=False, + action_delete=False, + ), + coursechapters=Permission( + action_create=True, + action_read=True, + action_update=False, + action_delete=False, + ), + activities=Permission( + action_create=True, + action_read=True, + action_update=False, + action_delete=False, + ), + roles=Permission( + action_create=False, + action_read=False, + action_update=False, + action_delete=False, + ), + dashboard=DashboardPermission( + action_access=True, + ), ), creation_date=str(datetime.now()), update_date=str(datetime.now()), @@ -244,20 +334,23 @@ def install_default_elements(db_session: Session): role_global_user = Role( name="User", - description="Standard User Role", + description="Read-Only Learner", role_type=RoleTypeEnum.TYPE_GLOBAL, role_uuid="role_global_user", - id=3, + id=4, rights=Rights( - courses=Permission( + courses=PermissionsWithOwn( action_create=False, action_read=True, + action_read_own=True, action_update=False, - action_delete=False, + action_update_own=False, + action_delete=True, + action_delete_own=True, ), users=Permission( - action_create=True, - action_read=True, + action_create=False, + action_read=False, action_update=False, action_delete=False, ), @@ -275,7 +368,7 @@ def install_default_elements(db_session: Session): ), organizations=Permission( action_create=False, - action_read=True, + action_read=False, action_update=False, action_delete=False, ), @@ -288,9 +381,18 @@ def install_default_elements(db_session: Session): activities=Permission( action_create=False, action_read=True, + action_update=False, + action_delete=False, + ), + roles=Permission( + action_create=False, + action_read=False, action_update=False, action_delete=False, ), + dashboard=DashboardPermission( + action_access=False, + ), ), creation_date=str(datetime.now()), update_date=str(datetime.now()), @@ -299,11 +401,13 @@ def install_default_elements(db_session: Session): # Serialize rights to JSON role_global_admin.rights = role_global_admin.rights.dict() # type: ignore role_global_maintainer.rights = role_global_maintainer.rights.dict() # type: ignore + role_global_instructor.rights = role_global_instructor.rights.dict() # type: ignore role_global_user.rights = role_global_user.rights.dict() # type: ignore # Insert roles in DB db_session.add(role_global_admin) db_session.add(role_global_maintainer) + db_session.add(role_global_instructor) db_session.add(role_global_user) # commit changes