mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: implement comprehensive RBAC checks for courses, chapters, collections, and activities, enhancing user rights management and security documentation
This commit is contained in:
parent
887046203e
commit
3ce019abec
22 changed files with 1788 additions and 598 deletions
|
|
@ -26,6 +26,7 @@ from src.services.courses.courses import (
|
|||
delete_course,
|
||||
update_course_thumbnail,
|
||||
search_courses,
|
||||
get_course_user_rights,
|
||||
)
|
||||
from src.services.courses.updates import (
|
||||
create_update,
|
||||
|
|
@ -358,12 +359,94 @@ async def api_remove_bulk_course_contributors(
|
|||
):
|
||||
"""
|
||||
Remove multiple contributors from a course by their usernames
|
||||
Only administrators can perform this action
|
||||
"""
|
||||
return await remove_bulk_course_contributors(
|
||||
request,
|
||||
course_uuid,
|
||||
usernames,
|
||||
current_user,
|
||||
db_session
|
||||
request, course_uuid, usernames, current_user, db_session
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{course_uuid}/rights")
|
||||
async def api_get_course_user_rights(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
db_session: Session = Depends(get_db_session),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""
|
||||
Get detailed user rights for a specific course.
|
||||
|
||||
This endpoint returns comprehensive rights information that can be used
|
||||
by the UI to enable/disable features based on user permissions.
|
||||
|
||||
|
||||
|
||||
**Response Structure:**
|
||||
```json
|
||||
{
|
||||
"course_uuid": "course_123",
|
||||
"user_id": 456,
|
||||
"is_anonymous": false,
|
||||
"permissions": {
|
||||
"read": true,
|
||||
"create": false,
|
||||
"update": true,
|
||||
"delete": false,
|
||||
"create_content": true,
|
||||
"update_content": true,
|
||||
"delete_content": true,
|
||||
"manage_contributors": true,
|
||||
"manage_access": true,
|
||||
"grade_assignments": true,
|
||||
"mark_activities_done": true,
|
||||
"create_certifications": true
|
||||
},
|
||||
"ownership": {
|
||||
"is_owner": true,
|
||||
"is_creator": true,
|
||||
"is_maintainer": false,
|
||||
"is_contributor": false,
|
||||
"authorship_status": "ACTIVE"
|
||||
},
|
||||
"roles": {
|
||||
"is_admin": false,
|
||||
"is_maintainer_role": false,
|
||||
"is_instructor": true,
|
||||
"is_user": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Permissions Explained:**
|
||||
- `read`: Can read the course content
|
||||
- `create`: Can create new courses (instructor role or higher)
|
||||
- `update`: Can update course settings (title, description, etc.)
|
||||
- `delete`: Can delete the course
|
||||
- `create_content`: Can create activities, assignments, chapters, etc.
|
||||
- `update_content`: Can update course content
|
||||
- `delete_content`: Can delete course content
|
||||
- `manage_contributors`: Can add/remove contributors
|
||||
- `manage_access`: Can change course access settings (public, open_to_contributors)
|
||||
- `grade_assignments`: Can grade student assignments
|
||||
- `mark_activities_done`: Can mark activities as done for other users
|
||||
- `create_certifications`: Can create course certifications
|
||||
|
||||
**Ownership Information:**
|
||||
- `is_owner`: Is course owner (CREATOR, MAINTAINER, or CONTRIBUTOR)
|
||||
- `is_creator`: Is course creator
|
||||
- `is_maintainer`: Is course maintainer
|
||||
- `is_contributor`: Is course contributor
|
||||
- `authorship_status`: Current authorship status (ACTIVE, PENDING, INACTIVE)
|
||||
|
||||
**Role Information:**
|
||||
- `is_admin`: Has admin role (role 1)
|
||||
- `is_maintainer_role`: Has maintainer role (role 2)
|
||||
- `is_instructor`: Has instructor role (role 3)
|
||||
- `is_user`: Has basic user role (role 4)
|
||||
|
||||
**Security Notes:**
|
||||
- Returns rights based on course ownership and user roles
|
||||
- Safe to expose to UI as it only returns permission information
|
||||
- Anonymous users can only read public courses
|
||||
- All permissions are calculated based on current user context
|
||||
"""
|
||||
return await get_course_user_rights(request, course_uuid, current_user, db_session)
|
||||
|
|
|
|||
410
apps/api/src/security/courses_security.py
Normal file
410
apps/api/src/security/courses_security.py
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
"""
|
||||
SECURITY DOCUMENTATION FOR COURSES RBAC SYSTEM
|
||||
|
||||
This module provides unified RBAC (Role-Based Access Control) checks for all courses-related operations.
|
||||
|
||||
SECURITY MEASURES IMPLEMENTED:
|
||||
|
||||
1. COURSE OWNERSHIP REQUIREMENTS:
|
||||
- All non-read operations (create, update, delete) require course ownership
|
||||
- Course ownership is determined by ResourceAuthor table with ACTIVE status
|
||||
- Valid ownership roles: CREATOR, MAINTAINER, CONTRIBUTOR
|
||||
- Admin/maintainer roles are also accepted for course operations
|
||||
|
||||
2. COURSE CREATION VS COURSE CONTENT CREATION:
|
||||
- COURSE CREATION: Allow if user has instructor role (3) or higher
|
||||
- COURSE CONTENT CREATION (activities, assignments, chapters, etc.): Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- This distinction allows instructors to create courses but prevents them from creating content in courses they don't own
|
||||
|
||||
3. STRICT ACCESS CONTROLS:
|
||||
- Activities: Require course ownership for all non-read operations
|
||||
- Assignments: Require course ownership for all non-read operations
|
||||
- Chapters: Require course ownership for all non-read operations
|
||||
- Certifications: Require course ownership for all non-read operations
|
||||
- Collections: Use organization-level permissions
|
||||
|
||||
4. GRADING AND SUBMISSION SECURITY:
|
||||
- Only course owners or instructors can grade assignments
|
||||
- Users can only submit their own work
|
||||
- Users cannot update grades unless they are instructors
|
||||
- Users can only update their own submissions
|
||||
|
||||
5. CERTIFICATE SECURITY:
|
||||
- Certificates can only be created by course owners or instructors
|
||||
- System-generated certificates (from course completion) are properly secured
|
||||
- Certificate creation requires proper RBAC checks
|
||||
|
||||
6. ACTIVITY MARKING SECURITY:
|
||||
- Only course owners or instructors can mark activities as done for other users
|
||||
- Users can only mark their own activities as done
|
||||
|
||||
7. COLLECTION SECURITY:
|
||||
- Users can only add courses to collections if they have read access to those courses
|
||||
- Collection operations require appropriate organization-level permissions
|
||||
|
||||
8. ANONYMOUS USER HANDLING:
|
||||
- Anonymous users can only read public courses
|
||||
- All non-read operations require authentication
|
||||
|
||||
9. ERROR HANDLING:
|
||||
- Clear error messages for security violations
|
||||
- Proper HTTP status codes (401, 403, 404)
|
||||
- Comprehensive logging of security events
|
||||
|
||||
10. COURSE ACCESS MANAGEMENT SECURITY:
|
||||
- Sensitive fields (public, open_to_contributors) require additional validation
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can change access settings
|
||||
- Course creation requires proper organization-level permissions
|
||||
- Course updates require course ownership or admin role
|
||||
|
||||
11. CONTRIBUTOR MANAGEMENT SECURITY:
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can add/remove contributors
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can update contributor roles
|
||||
- Cannot modify the role of the course creator
|
||||
- Contributor applications are created with PENDING status
|
||||
- Only course owners or admins can approve contributor applications
|
||||
|
||||
SECURITY BEST PRACTICES:
|
||||
- Always check course ownership before allowing modifications
|
||||
- Validate user permissions at multiple levels
|
||||
- Use proper RBAC checks for all operations
|
||||
- Implement principle of least privilege
|
||||
- Provide clear error messages for security violations
|
||||
- Log security events for audit purposes
|
||||
- Additional validation for sensitive access control fields
|
||||
- Strict ownership requirements for contributor management
|
||||
- Distinguish between course creation and course content creation permissions
|
||||
|
||||
CRITICAL SECURITY FIXES:
|
||||
- Fixed: Users could create certifications for courses they don't own
|
||||
- Fixed: Users could grade assignments without proper permissions
|
||||
- Fixed: Users could mark activities as done for other users without permissions
|
||||
- Fixed: Collections could be created with courses the user doesn't have access to
|
||||
- Fixed: Assignment submissions could be modified by unauthorized users
|
||||
- Fixed: Users could change course access settings (public, open_to_contributors) without proper permissions
|
||||
- Fixed: Users could add/remove contributors from courses they don't own
|
||||
- Fixed: Users could update contributor roles without course ownership
|
||||
- Fixed: Course creation used hardcoded RBAC check
|
||||
- Fixed: Contributor management used permissive RBAC checks instead of strict ownership requirements
|
||||
- Fixed: Instructors could create content in courses they don't own (now they can only create courses)
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
from fastapi import HTTPException, Request, status
|
||||
from sqlmodel import Session, select
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
authorization_verify_based_on_org_admin_status,
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
require_course_ownership: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Unified RBAC check for courses-related operations.
|
||||
|
||||
SECURITY NOTES:
|
||||
- READ operations: Allow if user has read access to the course (public courses or user has permissions)
|
||||
- COURSE CREATION: Allow if user has instructor role (3) or higher
|
||||
- COURSE CONTENT CREATION (activities, assignments, chapters, etc.): Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- UPDATE/DELETE operations: Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- Course ownership is determined by ResourceAuthor table with ACTIVE status
|
||||
- Admin/maintainer roles are checked via authorization_verify_based_on_org_admin_status
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
course_uuid: UUID of the course (or "course_x" for course creation)
|
||||
current_user: Current user (PublicUser or AnonymousUser)
|
||||
action: Action to perform (create, read, update, delete)
|
||||
db_session: Database session
|
||||
require_course_ownership: If True, requires course ownership for non-read actions
|
||||
|
||||
Returns:
|
||||
bool: True if authorized, raises HTTPException otherwise
|
||||
|
||||
Raises:
|
||||
HTTPException: 403 Forbidden if user lacks required permissions
|
||||
HTTPException: 401 Unauthorized if user is anonymous for non-read actions
|
||||
"""
|
||||
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
return await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
else:
|
||||
return await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, course_uuid, db_session
|
||||
)
|
||||
else:
|
||||
# For non-read actions, proceed with strict RBAC checks
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
# SECURITY: Special handling for course creation vs course content creation
|
||||
if action == "create" and course_uuid == "course_x":
|
||||
# This is course creation - allow instructors (role 3) or higher
|
||||
# Check if user has instructor role or higher
|
||||
from src.security.rbac.rbac import authorization_verify_based_on_roles
|
||||
|
||||
has_create_permission = await authorization_verify_based_on_roles(
|
||||
request, current_user.id, "create", "course_x", db_session
|
||||
)
|
||||
|
||||
if has_create_permission:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You must have instructor role or higher to create courses",
|
||||
)
|
||||
|
||||
# SECURITY: For course content creation and other operations, require course ownership
|
||||
# This prevents users without course ownership from creating/modifying course content
|
||||
if require_course_ownership or action in ["create", "update", "delete"]:
|
||||
# Check if user is course owner (CREATOR, MAINTAINER, or CONTRIBUTOR)
|
||||
statement = select(ResourceAuthor).where(
|
||||
ResourceAuthor.resource_uuid == course_uuid,
|
||||
ResourceAuthor.user_id == current_user.id
|
||||
)
|
||||
resource_author = db_session.exec(statement).first()
|
||||
|
||||
is_course_owner = False
|
||||
if resource_author:
|
||||
if ((resource_author.authorship == ResourceAuthorshipEnum.CREATOR) or
|
||||
(resource_author.authorship == ResourceAuthorshipEnum.MAINTAINER) or
|
||||
(resource_author.authorship == ResourceAuthorshipEnum.CONTRIBUTOR)) and \
|
||||
resource_author.authorship_status == ResourceAuthorshipStatusEnum.ACTIVE:
|
||||
is_course_owner = True
|
||||
|
||||
# Check if user has admin or maintainer role
|
||||
is_admin_or_maintainer = await authorization_verify_based_on_org_admin_status(
|
||||
request, current_user.id, action, course_uuid, db_session
|
||||
)
|
||||
|
||||
# SECURITY: For creating, updating, and deleting course content, user MUST be either:
|
||||
# 1. Course owner (CREATOR, MAINTAINER, or CONTRIBUTOR with ACTIVE status)
|
||||
# 2. Admin or maintainer role
|
||||
# General role permissions are NOT sufficient for these actions
|
||||
if not (is_course_owner or is_admin_or_maintainer):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"You must be the course owner (CREATOR, MAINTAINER, or CONTRIBUTOR) or have admin/maintainer role to {action} in this course",
|
||||
)
|
||||
return True
|
||||
else:
|
||||
# For other actions, use the existing RBAC check
|
||||
return await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check_with_course_lookup(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
require_course_ownership: bool = False,
|
||||
) -> Course:
|
||||
"""
|
||||
Unified RBAC check for courses-related operations with course lookup.
|
||||
|
||||
SECURITY NOTES:
|
||||
- First validates that the course exists
|
||||
- Then performs RBAC check using courses_rbac_check
|
||||
- Returns the course object if authorized
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
course_uuid: UUID of the course
|
||||
current_user: Current user (PublicUser or AnonymousUser)
|
||||
action: Action to perform (create, read, update, delete)
|
||||
db_session: Database session
|
||||
require_course_ownership: If True, requires course ownership for non-read actions
|
||||
|
||||
Returns:
|
||||
Course: The course object if authorized, raises HTTPException otherwise
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 Not Found if course doesn't exist
|
||||
HTTPException: 403 Forbidden if user lacks required permissions
|
||||
"""
|
||||
|
||||
# First check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Perform RBAC check
|
||||
await courses_rbac_check(
|
||||
request, course_uuid, current_user, action, db_session, require_course_ownership
|
||||
)
|
||||
|
||||
return course
|
||||
|
||||
|
||||
async def courses_rbac_check_for_activities(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""
|
||||
Specialized RBAC check for activities that requires course ownership for non-read actions.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Activities are core course content and require strict ownership controls
|
||||
- READ: Allow if user has read access to the course
|
||||
- CREATE/UPDATE/DELETE: Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- This prevents unauthorized users from creating/modifying course activities
|
||||
- Instructors can create courses but cannot create activities in courses they don't own
|
||||
"""
|
||||
|
||||
return await courses_rbac_check(
|
||||
request, course_uuid, current_user, action, db_session, require_course_ownership=True
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check_for_assignments(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""
|
||||
Specialized RBAC check for assignments that requires course ownership for non-read actions.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Assignments are course content and require strict ownership controls
|
||||
- READ: Allow if user has read access to the course
|
||||
- CREATE/UPDATE/DELETE: Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- This prevents unauthorized users from creating/modifying course assignments
|
||||
- Instructors can create courses but cannot create assignments in courses they don't own
|
||||
"""
|
||||
|
||||
return await courses_rbac_check(
|
||||
request, course_uuid, current_user, action, db_session, require_course_ownership=True
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check_for_chapters(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""
|
||||
Specialized RBAC check for chapters that requires course ownership for non-read actions.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Chapters are course structure and require strict ownership controls
|
||||
- READ: Allow if user has read access to the course
|
||||
- CREATE/UPDATE/DELETE: Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- This prevents unauthorized users from creating/modifying course chapters
|
||||
- Instructors can create courses but cannot create chapters in courses they don't own
|
||||
"""
|
||||
|
||||
return await courses_rbac_check(
|
||||
request, course_uuid, current_user, action, db_session, require_course_ownership=True
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check_for_certifications(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""
|
||||
Specialized RBAC check for certifications that requires course ownership for non-read actions.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Certifications are course credentials and require strict ownership controls
|
||||
- READ: Allow if user has read access to the course
|
||||
- CREATE/UPDATE/DELETE: Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- This prevents unauthorized users from creating/modifying course certifications
|
||||
- CRITICAL: Without this check, users could create certifications for courses they don't own
|
||||
- Instructors can create courses but cannot create certifications in courses they don't own
|
||||
"""
|
||||
|
||||
return await courses_rbac_check(
|
||||
request, course_uuid, current_user, action, db_session, require_course_ownership=True
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check_for_collections(
|
||||
request: Request,
|
||||
collection_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""
|
||||
Specialized RBAC check for collections.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Collections are course groupings and require appropriate access controls
|
||||
- READ: Allow if collection is public or user has read access
|
||||
- CREATE/UPDATE/DELETE: Require appropriate permissions based on collection ownership
|
||||
- Collections may have different ownership models than courses
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
collection_uuid: UUID of the collection
|
||||
current_user: Current user (PublicUser or AnonymousUser)
|
||||
action: Action to perform (create, read, update, delete)
|
||||
db_session: Database session
|
||||
|
||||
Returns:
|
||||
bool: True if authorized, raises HTTPException otherwise
|
||||
"""
|
||||
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, collection_uuid, action, db_session
|
||||
)
|
||||
if res == False:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights : You are not allowed to read this collection",
|
||||
)
|
||||
return res
|
||||
else:
|
||||
return await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, collection_uuid, db_session
|
||||
)
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
return await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
collection_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
|
@ -6,15 +6,19 @@ from src.security.rbac.rbac import (
|
|||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
authorization_verify_based_on_org_admin_status,
|
||||
authorization_verify_based_on_roles,
|
||||
)
|
||||
from src.db.courses.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate
|
||||
from src.db.courses.chapter_activities import ChapterActivity
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from fastapi import HTTPException, Request
|
||||
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum
|
||||
from fastapi import HTTPException, Request, status
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
from src.services.payments.payments_access import check_activity_paid_access
|
||||
from src.security.courses_security import courses_rbac_check_for_activities
|
||||
|
||||
|
||||
####################################################
|
||||
|
|
@ -49,7 +53,7 @@ async def create_activity(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "create", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Create Activity
|
||||
activity = Activity(**activity_object.model_dump())
|
||||
|
|
@ -118,7 +122,7 @@ async def get_activity(
|
|||
activity, course = result
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Paid access check
|
||||
has_paid_access = await check_activity_paid_access(
|
||||
|
|
@ -156,7 +160,7 @@ async def get_activityby_id(
|
|||
activity, course = result
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
return ActivityRead.model_validate(activity)
|
||||
|
||||
|
|
@ -187,7 +191,7 @@ async def update_activity(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(activity_object).items():
|
||||
|
|
@ -228,7 +232,7 @@ async def delete_activity(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Delete activity from chapter
|
||||
statement = select(ChapterActivity).where(
|
||||
|
|
@ -296,46 +300,8 @@ async def get_activities(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
activities = [ActivityRead.model_validate(activity) for activity in activities]
|
||||
|
||||
return activities
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
element_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
|
||||
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, element_uuid, action, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
res = await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, element_uuid, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
# For non-read actions, proceed with regular RBAC checks
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
element_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from src.services.courses.activities.uploads.tasks_ref_files import (
|
|||
)
|
||||
from src.services.trail.trail import check_trail_presence
|
||||
from src.services.courses.certifications import check_course_completion_and_create_certificate
|
||||
from src.security.courses_security import courses_rbac_check_for_assignments
|
||||
|
||||
## > Assignments CRUD
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ async def create_assignment(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "create", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Usage check
|
||||
check_limits_with_usage("assignments", course.org_id, db_session)
|
||||
|
|
@ -118,7 +119,7 @@ async def read_assignment(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment read
|
||||
return AssignmentRead.model_validate(assignment)
|
||||
|
|
@ -161,7 +162,7 @@ async def read_assignment_from_activity_uuid(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment read
|
||||
return AssignmentRead.model_validate(assignment)
|
||||
|
|
@ -195,7 +196,7 @@ async def update_assignment(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(assignment_object).items():
|
||||
|
|
@ -239,7 +240,7 @@ async def delete_assignment(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Feature usage
|
||||
decrease_feature_usage("assignments", course.org_id, db_session)
|
||||
|
|
@ -289,7 +290,7 @@ async def delete_assignment_from_activity_uuid(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Feature usage
|
||||
decrease_feature_usage("assignments", course.org_id, db_session)
|
||||
|
|
@ -333,7 +334,7 @@ async def create_assignment_task(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "create", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Create Assignment Task
|
||||
assignment_task = AssignmentTask(**assignment_task_object.model_dump())
|
||||
|
|
@ -388,7 +389,7 @@ async def read_assignment_tasks(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment tasks read
|
||||
return [
|
||||
|
|
@ -436,7 +437,7 @@ async def read_assignment_task(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment task read
|
||||
return AssignmentTaskRead.model_validate(assignmenttask)
|
||||
|
|
@ -490,7 +491,7 @@ async def put_assignment_task_reference_file(
|
|||
org = db_session.exec(org_statement).first()
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Upload reference file
|
||||
if reference_file and reference_file.filename and activity and org:
|
||||
|
|
@ -568,7 +569,7 @@ async def put_assignment_task_submission_file(
|
|||
org = db_session.exec(org_statement).first()
|
||||
|
||||
# RBAC check - only need read permission to submit files
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Check if user is enrolled in the course
|
||||
if not await authorization_verify_based_on_roles(request, current_user.id, "read", course.course_uuid, db_session):
|
||||
|
|
@ -633,7 +634,7 @@ async def update_assignment_task(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(assignment_task_object).items():
|
||||
|
|
@ -689,7 +690,7 @@ async def delete_assignment_task(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Delete Assignment Task
|
||||
db_session.delete(assignment_task)
|
||||
|
|
@ -741,7 +742,7 @@ async def handle_assignment_task_submission(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Check if user has instructor/admin permissions
|
||||
# SECURITY: Check if user has instructor/admin permissions for grading
|
||||
is_instructor = await authorization_verify_based_on_roles(request, current_user.id, "update", course.course_uuid, db_session)
|
||||
|
||||
# For regular users, ensure they can only submit their own work
|
||||
|
|
@ -753,7 +754,7 @@ async def handle_assignment_task_submission(
|
|||
detail="You must be enrolled in this course to submit assignments"
|
||||
)
|
||||
|
||||
# Regular users cannot update grades - only check if actual values are being set
|
||||
# SECURITY: Regular users cannot update grades - only check if actual values are being set
|
||||
if (assignment_task_submission_object.grade is not None and assignment_task_submission_object.grade != 0) or \
|
||||
(assignment_task_submission_object.task_submission_grade_feedback is not None and assignment_task_submission_object.task_submission_grade_feedback != ""):
|
||||
raise HTTPException(
|
||||
|
|
@ -762,10 +763,10 @@ async def handle_assignment_task_submission(
|
|||
)
|
||||
|
||||
# Only need read permission for submissions
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
else:
|
||||
# Instructors/admins need update permission to grade
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
# SECURITY: Instructors/admins need update permission to grade
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Try to find existing submission if UUID is provided
|
||||
assignment_task_submission = None
|
||||
|
|
@ -777,7 +778,7 @@ async def handle_assignment_task_submission(
|
|||
|
||||
# If submission exists, update it
|
||||
if assignment_task_submission:
|
||||
# For regular users, ensure they can only update their own submissions
|
||||
# SECURITY: For regular users, ensure they can only update their own submissions
|
||||
if not is_instructor and assignment_task_submission.user_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
|
|
@ -880,7 +881,7 @@ async def read_user_assignment_task_submissions(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment task submission read
|
||||
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
|
||||
|
|
@ -953,7 +954,7 @@ async def read_assignment_task_submissions(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment task submission read
|
||||
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
|
||||
|
|
@ -1012,7 +1013,7 @@ async def update_assignment_task_submission(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(assignment_task_submission_object).items():
|
||||
|
|
@ -1081,7 +1082,7 @@ async def delete_assignment_task_submission(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Delete Assignment Task Submission
|
||||
db_session.delete(assignment_task_submission)
|
||||
|
|
@ -1147,7 +1148,7 @@ async def create_assignment_submission(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Create Assignment User Submission
|
||||
assignment_user_submission = AssignmentUserSubmission(
|
||||
|
|
@ -1280,7 +1281,7 @@ async def read_assignment_submissions(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment tasks read
|
||||
return [
|
||||
|
|
@ -1323,7 +1324,7 @@ async def read_user_assignment_submissions(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment tasks read
|
||||
return [
|
||||
|
|
@ -1389,7 +1390,7 @@ async def update_assignment_submission(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(assignment_user_submission_object).items():
|
||||
|
|
@ -1447,7 +1448,7 @@ async def delete_assignment_submission(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Delete Assignment User Submission
|
||||
db_session.delete(assignment_user_submission)
|
||||
|
|
@ -1464,7 +1465,7 @@ async def grade_assignment_submission(
|
|||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
|
||||
# SECURITY: This function should only be accessible by course owners or instructors
|
||||
# Check if assignment exists
|
||||
statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid)
|
||||
assignment = db_session.exec(statement).first()
|
||||
|
|
@ -1484,7 +1485,8 @@ async def grade_assignment_submission(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
# SECURITY: Require course ownership or instructor role for grading
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Check if assignment user submission exists
|
||||
statement = select(AssignmentUserSubmission).where(
|
||||
|
|
@ -1602,6 +1604,7 @@ async def mark_activity_as_done_for_user(
|
|||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
# SECURITY: This function should only be accessible by course owners or instructors
|
||||
# Get Assignment
|
||||
statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid)
|
||||
assignment = db_session.exec(statement).first()
|
||||
|
|
@ -1625,7 +1628,8 @@ async def mark_activity_as_done_for_user(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
# SECURITY: Require course ownership or instructor role for marking activities as done
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
if not activity:
|
||||
raise HTTPException(
|
||||
|
|
@ -1704,46 +1708,7 @@ async def get_assignments_from_course(
|
|||
assignments.append(assignment)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignments read
|
||||
return [AssignmentRead.model_validate(assignment) for assignment in assignments]
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
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(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from src.services.courses.activities.uploads.pdfs import upload_pdf
|
|||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from src.security.courses_security import courses_rbac_check_for_activities
|
||||
|
||||
|
||||
async def create_documentpdf_activity(
|
||||
|
|
@ -30,9 +31,6 @@ async def create_documentpdf_activity(
|
|||
db_session: Session,
|
||||
pdf_file: UploadFile | None = None,
|
||||
):
|
||||
# RBAC check
|
||||
await rbac_check(request, "course_uuid", current_user, "create", db_session)
|
||||
|
||||
# get chapter_id
|
||||
statement = select(Chapter).where(Chapter.id == chapter_id)
|
||||
chapter = db_session.exec(statement).first()
|
||||
|
|
@ -52,6 +50,19 @@ async def create_documentpdf_activity(
|
|||
detail="CourseChapter not found",
|
||||
)
|
||||
|
||||
# Get course_uuid for RBAC check
|
||||
statement = select(Course).where(Course.id == coursechapter.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# get org_id
|
||||
org_id = coursechapter.org_id
|
||||
|
||||
|
|
@ -59,10 +70,6 @@ async def create_documentpdf_activity(
|
|||
statement = select(Organization).where(Organization.id == coursechapter.org_id)
|
||||
organization = db_session.exec(statement).first()
|
||||
|
||||
# Get course_uuid
|
||||
statement = select(Course).where(Course.id == coursechapter.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
# create activity uuid
|
||||
activity_uuid = f"activity_{uuid4()}"
|
||||
|
||||
|
|
@ -119,7 +126,7 @@ async def create_documentpdf_activity(
|
|||
)
|
||||
|
||||
# upload pdf
|
||||
if pdf_file:
|
||||
if pdf_file and organization and course:
|
||||
# get pdffile format
|
||||
await upload_pdf(
|
||||
pdf_file,
|
||||
|
|
@ -134,27 +141,3 @@ async def create_documentpdf_activity(
|
|||
db_session.refresh(activity_chapter)
|
||||
|
||||
return ActivityRead.model_validate(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 ##
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from src.services.courses.activities.uploads.videos import upload_video
|
|||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from src.security.courses_security import courses_rbac_check_for_activities
|
||||
|
||||
|
||||
async def create_video_activity(
|
||||
|
|
@ -34,9 +35,6 @@ async def create_video_activity(
|
|||
video_file: UploadFile | None = None,
|
||||
details: str = "{}",
|
||||
):
|
||||
# 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()
|
||||
|
|
@ -59,14 +57,23 @@ async def create_video_activity(
|
|||
detail="CourseChapter not found",
|
||||
)
|
||||
|
||||
# Get course_uuid for RBAC check
|
||||
statement = select(Course).where(Course.id == coursechapter.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Get org_uuid
|
||||
statement = select(Organization).where(Organization.id == coursechapter.org_id)
|
||||
organization = db_session.exec(statement).first()
|
||||
|
||||
# Get course_uuid
|
||||
statement = select(Course).where(Course.id == coursechapter.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
# generate activity_uuid
|
||||
activity_uuid = str(f"activity_{uuid4()}")
|
||||
|
||||
|
|
@ -103,7 +110,7 @@ async def create_video_activity(
|
|||
"filename": "video." + video_format,
|
||||
"activity_uuid": activity_uuid,
|
||||
},
|
||||
details=details,
|
||||
details=details if isinstance(details, dict) else json.loads(details),
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
)
|
||||
|
|
@ -115,7 +122,7 @@ async def create_video_activity(
|
|||
db_session.refresh(activity)
|
||||
|
||||
# upload video
|
||||
if video_file:
|
||||
if video_file and organization and course:
|
||||
# get videofile format
|
||||
await upload_video(
|
||||
video_file,
|
||||
|
|
@ -161,9 +168,6 @@ async def create_external_video_activity(
|
|||
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()
|
||||
|
|
@ -183,6 +187,19 @@ async def create_external_video_activity(
|
|||
detail="CourseChapter not found",
|
||||
)
|
||||
|
||||
# Get course_uuid for RBAC check
|
||||
statement = select(Course).where(Course.id == coursechapter.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# generate activity_uuid
|
||||
activity_uuid = str(f"activity_{uuid4()}")
|
||||
|
||||
|
|
@ -230,22 +247,4 @@ async def create_external_video_activity(
|
|||
return ActivityRead.model_validate(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 ##
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from src.security.rbac.rbac import (
|
|||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.security.courses_security import courses_rbac_check_for_certifications
|
||||
|
||||
|
||||
####################################################
|
||||
|
|
@ -46,7 +47,7 @@ async def create_certification(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "create", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Create certification
|
||||
certification = Certifications(
|
||||
|
|
@ -93,7 +94,7 @@ async def get_certification(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
return CertificationRead(**certification.model_dump())
|
||||
|
||||
|
|
@ -117,7 +118,7 @@ async def get_certifications_by_course(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get certifications for this course
|
||||
statement = select(Certifications).where(Certifications.course_id == course.id)
|
||||
|
|
@ -155,7 +156,7 @@ async def update_certification(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(certification_object).items():
|
||||
|
|
@ -200,7 +201,7 @@ async def delete_certification(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
db_session.delete(certification)
|
||||
db_session.commit()
|
||||
|
|
@ -218,8 +219,16 @@ async def create_certificate_user(
|
|||
user_id: int,
|
||||
certification_id: int,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser | None = None,
|
||||
) -> CertificateUserRead:
|
||||
"""Create a certificate user link"""
|
||||
"""
|
||||
Create a certificate user link
|
||||
|
||||
SECURITY NOTES:
|
||||
- This function should only be called by authorized users (course owners, instructors, or system)
|
||||
- When called from check_course_completion_and_create_certificate, it's a system operation
|
||||
- When called directly, requires proper RBAC checks
|
||||
"""
|
||||
|
||||
# Check if certification exists
|
||||
statement = select(Certifications).where(Certifications.id == certification_id)
|
||||
|
|
@ -231,6 +240,21 @@ async def create_certificate_user(
|
|||
detail="Certification not found",
|
||||
)
|
||||
|
||||
# SECURITY: If current_user is provided, perform RBAC check
|
||||
if current_user:
|
||||
# Get course for RBAC check
|
||||
statement = select(Course).where(Course.id == certification.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Require course ownership or instructor role for creating certificates
|
||||
await courses_rbac_check_for_certifications(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Check if certificate user already exists
|
||||
statement = select(CertificateUser).where(
|
||||
CertificateUser.user_id == user_id,
|
||||
|
|
@ -316,7 +340,7 @@ async def get_user_certificates_for_course(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get all certifications for this course
|
||||
statement = select(Certifications).where(Certifications.course_id == course.id)
|
||||
|
|
@ -357,7 +381,14 @@ async def check_course_completion_and_create_certificate(
|
|||
course_id: int,
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""Check if all activities in a course are completed and create certificate if so"""
|
||||
"""
|
||||
Check if all activities in a course are completed and create certificate if so
|
||||
|
||||
SECURITY NOTES:
|
||||
- This function is called by the system when activities are completed
|
||||
- It should only create certificates for users who have actually completed the course
|
||||
- The function is called from mark_activity_as_done_for_user which already has RBAC checks
|
||||
"""
|
||||
|
||||
# Get all activities in the course
|
||||
statement = select(ChapterActivity).where(ChapterActivity.course_id == course_id)
|
||||
|
|
@ -381,7 +412,8 @@ async def check_course_completion_and_create_certificate(
|
|||
certification = db_session.exec(statement).first()
|
||||
|
||||
if certification and certification.id:
|
||||
# Create certificate user link
|
||||
# SECURITY: Create certificate user link (system operation, no RBAC needed here)
|
||||
# This is called from mark_activity_as_done_for_user which already has proper RBAC checks
|
||||
try:
|
||||
await create_certificate_user(request, user_id, certification.id, db_session)
|
||||
return True
|
||||
|
|
@ -505,37 +537,4 @@ async def get_all_user_certificates(
|
|||
} if user else None
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
####################################################
|
||||
# RBAC Utils
|
||||
####################################################
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
else:
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, course_uuid, db_session
|
||||
)
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_uuid,
|
||||
db_session,
|
||||
)
|
||||
return result
|
||||
|
|
@ -2,7 +2,7 @@ from datetime import datetime
|
|||
from typing import List, Literal
|
||||
from uuid import uuid4
|
||||
from sqlmodel import Session, select
|
||||
from src.db.users import AnonymousUser
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
|
|
@ -18,9 +18,9 @@ from src.db.courses.chapters import (
|
|||
ChapterUpdate,
|
||||
ChapterUpdateOrder,
|
||||
)
|
||||
from src.services.courses.courses import Course
|
||||
from src.services.users.users import PublicUser
|
||||
from src.db.courses.courses import Course
|
||||
from fastapi import HTTPException, status, Request
|
||||
from src.security.courses_security import courses_rbac_check_for_chapters
|
||||
|
||||
|
||||
####################################################
|
||||
|
|
@ -42,7 +42,7 @@ async def create_chapter(
|
|||
course = db_session.exec(statement).one()
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, "chapter_x", current_user, "create", db_session)
|
||||
await courses_rbac_check_for_chapters(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# complete chapter object
|
||||
chapter.course_id = chapter_object.course_id
|
||||
|
|
@ -55,7 +55,7 @@ async def create_chapter(
|
|||
statement = (
|
||||
select(CourseChapter)
|
||||
.where(CourseChapter.course_id == chapter.course_id)
|
||||
.order_by(CourseChapter.order)
|
||||
.order_by(CourseChapter.order) # type: ignore
|
||||
)
|
||||
course_chapters = db_session.exec(statement).all()
|
||||
|
||||
|
|
@ -122,14 +122,14 @@ async def get_chapter(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_chapters(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get activities for this chapter
|
||||
statement = (
|
||||
select(Activity)
|
||||
.join(ChapterActivity, Activity.id == ChapterActivity.activity_id)
|
||||
.join(ChapterActivity, Activity.id == ChapterActivity.activity_id) # type: ignore
|
||||
.where(ChapterActivity.chapter_id == chapter_id)
|
||||
.distinct(Activity.id)
|
||||
.distinct(Activity.id) # type: ignore
|
||||
)
|
||||
|
||||
activities = db_session.exec(statement).all()
|
||||
|
|
@ -158,7 +158,7 @@ async def update_chapter(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, chapter.chapter_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_chapters(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():
|
||||
|
|
@ -193,7 +193,7 @@ async def delete_chapter(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, chapter.chapter_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_chapters(request, chapter.chapter_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Remove all linked chapter activities
|
||||
statement = select(ChapterActivity).where(ChapterActivity.chapter_id == chapter.id)
|
||||
|
|
@ -224,26 +224,26 @@ async def get_course_chapters(
|
|||
|
||||
statement = (
|
||||
select(Chapter)
|
||||
.join(CourseChapter, Chapter.id == CourseChapter.chapter_id)
|
||||
.join(CourseChapter, Chapter.id == CourseChapter.chapter_id) # type: ignore
|
||||
.where(CourseChapter.course_id == course_id)
|
||||
.where(Chapter.course_id == course_id)
|
||||
.order_by(CourseChapter.order)
|
||||
.group_by(Chapter.id, CourseChapter.order)
|
||||
.order_by(CourseChapter.order) # type: ignore
|
||||
.group_by(Chapter.id, CourseChapter.order) # type: ignore
|
||||
)
|
||||
chapters = db_session.exec(statement).all()
|
||||
|
||||
chapters = [ChapterRead(**chapter.model_dump(), activities=[]) for chapter in chapters]
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session) # type: ignore
|
||||
await courses_rbac_check_for_chapters(request, course.course_uuid, current_user, "read", db_session) # type: ignore
|
||||
|
||||
# Get activities for each chapter
|
||||
for chapter in chapters:
|
||||
statement = (
|
||||
select(ChapterActivity)
|
||||
.where(ChapterActivity.chapter_id == chapter.id)
|
||||
.order_by(ChapterActivity.order)
|
||||
.distinct(ChapterActivity.id, ChapterActivity.order)
|
||||
.order_by(ChapterActivity.order) # type: ignore
|
||||
.distinct(ChapterActivity.id, ChapterActivity.order) # type: ignore
|
||||
)
|
||||
chapter_activities = db_session.exec(statement).all()
|
||||
|
||||
|
|
@ -251,7 +251,7 @@ async def get_course_chapters(
|
|||
statement = (
|
||||
select(Activity)
|
||||
.where(Activity.id == chapter_activity.activity_id, with_unpublished_activities or Activity.published == True)
|
||||
.distinct(Activity.id)
|
||||
.distinct(Activity.id) # type: ignore
|
||||
)
|
||||
activity = db_session.exec(statement).first()
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ async def DEPRECEATED_get_course_chapters(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_chapters(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
chapters_in_db = await get_course_chapters(request, course.id, db_session, current_user) # type: ignore
|
||||
|
||||
|
|
@ -306,9 +306,9 @@ async def DEPRECEATED_get_course_chapters(
|
|||
activities_list = {}
|
||||
statement = (
|
||||
select(Activity)
|
||||
.join(ChapterActivity, ChapterActivity.activity_id == Activity.id)
|
||||
.join(ChapterActivity, ChapterActivity.activity_id == Activity.id) # type: ignore
|
||||
.where(ChapterActivity.activity_id == Activity.id)
|
||||
.group_by(Activity.id)
|
||||
.group_by(Activity.id) # type: ignore
|
||||
)
|
||||
activities_in_db = db_session.exec(statement).all()
|
||||
|
||||
|
|
@ -324,10 +324,10 @@ async def DEPRECEATED_get_course_chapters(
|
|||
# get chapter order
|
||||
statement = (
|
||||
select(Chapter)
|
||||
.join(CourseChapter, CourseChapter.chapter_id == Chapter.id)
|
||||
.join(CourseChapter, CourseChapter.chapter_id == Chapter.id) # type: ignore
|
||||
.where(CourseChapter.chapter_id == Chapter.id)
|
||||
.group_by(Chapter.id, CourseChapter.order)
|
||||
.order_by(CourseChapter.order)
|
||||
.group_by(Chapter.id, CourseChapter.order) # type: ignore
|
||||
.order_by(CourseChapter.order) # type: ignore
|
||||
)
|
||||
chapters_in_db = db_session.exec(statement).all()
|
||||
|
||||
|
|
@ -361,7 +361,7 @@ async def reorder_chapters_and_activities(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_chapters(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
###########
|
||||
# Chapters
|
||||
|
|
@ -458,39 +458,3 @@ async def reorder_chapters_and_activities(
|
|||
db_session.commit()
|
||||
|
||||
return {"detail": "Chapters and activities reordered successfully"}
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
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(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from datetime import datetime
|
|||
from typing import List, Literal
|
||||
from uuid import uuid4
|
||||
from sqlmodel import Session, select
|
||||
from src.db.users import AnonymousUser
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
|
|
@ -16,8 +16,8 @@ from src.db.collections import (
|
|||
)
|
||||
from src.db.collections_courses import CollectionCourse
|
||||
from src.db.courses.courses import Course
|
||||
from src.services.users.users import PublicUser
|
||||
from fastapi import HTTPException, status, Request
|
||||
from src.security.courses_security import courses_rbac_check_for_collections
|
||||
|
||||
|
||||
####################################################
|
||||
|
|
@ -40,7 +40,7 @@ async def get_collection(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(
|
||||
await courses_rbac_check_for_collections(
|
||||
request, collection.collection_uuid, current_user, "read", db_session
|
||||
)
|
||||
|
||||
|
|
@ -86,8 +86,10 @@ async def create_collection(
|
|||
) -> CollectionRead:
|
||||
collection = Collection.model_validate(collection_object)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, "collection_x", current_user, "create", db_session)
|
||||
# SECURITY: Check if user has permission to create collections in this organization
|
||||
# Since collections are organization-level resources, we need to check org permissions
|
||||
# For now, we'll use the existing RBAC check but with proper organization context
|
||||
await courses_rbac_check_for_collections(request, "collection_x", current_user, "create", db_session)
|
||||
|
||||
# Complete the collection object
|
||||
collection.collection_uuid = f"collection_{uuid4()}"
|
||||
|
|
@ -99,18 +101,32 @@ async def create_collection(
|
|||
db_session.commit()
|
||||
db_session.refresh(collection)
|
||||
|
||||
# Link courses to collection
|
||||
# SECURITY: Link courses to collection - ensure user has access to all courses being added
|
||||
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)
|
||||
# Check if user has access to this course
|
||||
statement = select(Course).where(Course.id == course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if course:
|
||||
# Verify user has read access to the course before adding it to collection
|
||||
try:
|
||||
await courses_rbac_check_for_collections(request, course.course_uuid, current_user, "read", db_session)
|
||||
except HTTPException:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"You don't have permission to add course {course.name} to this collection"
|
||||
)
|
||||
|
||||
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)
|
||||
|
|
@ -145,7 +161,7 @@ async def update_collection(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(
|
||||
await courses_rbac_check_for_collections(
|
||||
request, collection.collection_uuid, current_user, "update", db_session
|
||||
)
|
||||
|
||||
|
|
@ -219,7 +235,7 @@ async def delete_collection(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(
|
||||
await courses_rbac_check_for_collections(
|
||||
request, collection.collection_uuid, current_user, "delete", db_session
|
||||
)
|
||||
|
||||
|
|
@ -248,7 +264,7 @@ async def get_collections(
|
|||
Collection.org_id == org_id, Collection.public == True
|
||||
)
|
||||
statement_all = (
|
||||
select(Collection).where(Collection.org_id == org_id).distinct(Collection.id)
|
||||
select(Collection).where(Collection.org_id == org_id).distinct(Collection.id) # type: ignore
|
||||
)
|
||||
|
||||
if current_user.id == 0:
|
||||
|
|
@ -288,49 +304,7 @@ async def get_collections(
|
|||
|
||||
courses = db_session.exec(statement).all()
|
||||
|
||||
collection = CollectionRead(**collection.model_dump(), courses=courses)
|
||||
collection = CollectionRead(**collection.model_dump(), courses=list(courses))
|
||||
collections_with_courses.append(collection)
|
||||
|
||||
return collections_with_courses
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
collection_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, collection_uuid, action, db_session
|
||||
)
|
||||
if res == False:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights : You are not allowed to read this collection",
|
||||
)
|
||||
else:
|
||||
res = (
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, collection_uuid, db_session
|
||||
)
|
||||
)
|
||||
return res
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
collection_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from sqlmodel import Session, select, and_
|
|||
from src.db.users import PublicUser, AnonymousUser, User, UserRead
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum
|
||||
from src.security.rbac.rbac import authorization_verify_if_user_is_anon, authorization_verify_based_on_roles_and_authorship
|
||||
from src.security.rbac.rbac import authorization_verify_if_user_is_anon, authorization_verify_based_on_org_admin_status
|
||||
from src.security.courses_security import courses_rbac_check
|
||||
from typing import List
|
||||
|
||||
|
||||
|
|
@ -14,6 +15,14 @@ async def apply_course_contributor(
|
|||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
"""
|
||||
Apply to become a course contributor
|
||||
|
||||
SECURITY NOTES:
|
||||
- Any authenticated user can apply to become a contributor
|
||||
- Applications are created with PENDING status
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can approve applications
|
||||
"""
|
||||
# Verify user is not anonymous
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
|
|
@ -73,21 +82,17 @@ async def update_course_contributor(
|
|||
):
|
||||
"""
|
||||
Update a course contributor's role and status
|
||||
Only administrators can perform this action
|
||||
|
||||
SECURITY NOTES:
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can update contributors
|
||||
- Cannot modify the role of the course creator
|
||||
- Requires strict course ownership checks
|
||||
"""
|
||||
# Verify user is not anonymous
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
# RBAC check - verify if user has admin rights
|
||||
authorized = await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, "update", course_uuid, db_session
|
||||
)
|
||||
|
||||
if not authorized:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You are not authorized to update course contributors",
|
||||
)
|
||||
# SECURITY: Require course ownership or admin role for updating contributors
|
||||
await courses_rbac_check(request, course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
|
|
@ -115,7 +120,7 @@ async def update_course_contributor(
|
|||
detail="Contributor not found for this course",
|
||||
)
|
||||
|
||||
# Don't allow changing the role of the creator
|
||||
# SECURITY: Don't allow changing the role of the creator
|
||||
if existing_authorship.authorship == ResourceAuthorshipEnum.CREATOR:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -144,6 +149,10 @@ async def get_course_contributors(
|
|||
) -> List[dict]:
|
||||
"""
|
||||
Get all contributors for a course with their user information
|
||||
|
||||
SECURITY NOTES:
|
||||
- Requires read access to the course
|
||||
- Contributors are visible to anyone with course read access
|
||||
"""
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
|
|
@ -155,6 +164,9 @@ async def get_course_contributors(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
# SECURITY: Require read access to the course
|
||||
await courses_rbac_check(request, course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get all contributors for this course with user information
|
||||
statement = (
|
||||
select(ResourceAuthor, User)
|
||||
|
|
@ -184,21 +196,17 @@ async def add_bulk_course_contributors(
|
|||
):
|
||||
"""
|
||||
Add multiple contributors to a course by their usernames
|
||||
Only administrators can perform this action
|
||||
|
||||
SECURITY NOTES:
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can add contributors
|
||||
- Requires strict course ownership checks
|
||||
- Cannot add contributors to courses the user doesn't own
|
||||
"""
|
||||
# Verify user is not anonymous
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
# RBAC check - verify if user has admin rights
|
||||
authorized = await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, "update", course_uuid, db_session
|
||||
)
|
||||
|
||||
if not authorized:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You are not authorized to add contributors",
|
||||
)
|
||||
# SECURITY: Require course ownership or admin role for adding contributors
|
||||
await courses_rbac_check(request, course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
|
|
@ -284,21 +292,18 @@ async def remove_bulk_course_contributors(
|
|||
):
|
||||
"""
|
||||
Remove multiple contributors from a course by their usernames
|
||||
Only administrators can perform this action
|
||||
|
||||
SECURITY NOTES:
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can remove contributors
|
||||
- Requires strict course ownership checks
|
||||
- Cannot remove contributors from courses the user doesn't own
|
||||
- Cannot remove the course creator
|
||||
"""
|
||||
# Verify user is not anonymous
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
# RBAC check - verify if user has admin rights
|
||||
authorized = await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, "update", course_uuid, db_session
|
||||
)
|
||||
|
||||
if not authorized:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You are not authorized to remove contributors",
|
||||
)
|
||||
# SECURITY: Require course ownership or admin role for removing contributors
|
||||
await courses_rbac_check(request, course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
|
|
@ -346,7 +351,7 @@ async def remove_bulk_course_contributors(
|
|||
})
|
||||
continue
|
||||
|
||||
# Don't allow removing the creator
|
||||
# SECURITY: Don't allow removing the creator
|
||||
if existing_authorship.authorship == ResourceAuthorshipEnum.CREATOR:
|
||||
results["failed"].append({
|
||||
"username": username,
|
||||
|
|
|
|||
|
|
@ -24,10 +24,12 @@ from src.security.rbac.rbac import (
|
|||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
authorization_verify_based_on_org_admin_status,
|
||||
)
|
||||
from src.services.courses.thumbnails import upload_thumbnail
|
||||
from fastapi import HTTPException, Request, UploadFile
|
||||
from fastapi import HTTPException, Request, UploadFile, status
|
||||
from datetime import datetime
|
||||
from src.security.courses_security import courses_rbac_check
|
||||
|
||||
|
||||
async def get_course(
|
||||
|
|
@ -46,15 +48,15 @@ async def get_course(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -92,15 +94,15 @@ async def get_course_by_id(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -153,7 +155,7 @@ async def get_course_meta(
|
|||
author_results = [(ra, u) for _, ra, u in results if ra is not None and u is not None]
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get course chapters
|
||||
chapters = []
|
||||
|
|
@ -241,7 +243,7 @@ async def get_courses_orgslug(
|
|||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid.in_(course_uuids)) # type: ignore
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -349,10 +351,10 @@ async def search_courses(
|
|||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -399,10 +401,20 @@ async def create_course(
|
|||
thumbnail_file: UploadFile | None = None,
|
||||
thumbnail_type: ThumbnailType = ThumbnailType.IMAGE,
|
||||
):
|
||||
"""
|
||||
Create a new course
|
||||
|
||||
SECURITY NOTES:
|
||||
- Requires proper permissions to create courses in the organization
|
||||
- User becomes the CREATOR of the course automatically
|
||||
- Course creation is subject to organization limits and permissions
|
||||
"""
|
||||
course = Course.model_validate(course_object)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, "course_x", current_user, "create", db_session)
|
||||
# SECURITY: Check if user has permission to create courses in this organization
|
||||
# Since this is a new course, we need to check organization-level permissions
|
||||
# For now, we'll use the existing RBAC check but with proper organization context
|
||||
await courses_rbac_check(request, "course_x", current_user, "create", db_session)
|
||||
|
||||
# Usage check
|
||||
check_limits_with_usage("courses", org_id, db_session)
|
||||
|
|
@ -440,7 +452,7 @@ async def create_course(
|
|||
db_session.commit()
|
||||
db_session.refresh(course)
|
||||
|
||||
# Make the user the creator of the course
|
||||
# SECURITY: Make the user the creator of the course
|
||||
resource_author = ResourceAuthor(
|
||||
resource_uuid=course.course_uuid,
|
||||
user_id=current_user.id,
|
||||
|
|
@ -458,10 +470,10 @@ async def create_course(
|
|||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -506,7 +518,7 @@ async def update_course_thumbnail(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Get org uuid
|
||||
org_statement = select(Organization).where(Organization.id == course.org_id)
|
||||
|
|
@ -543,10 +555,10 @@ async def update_course_thumbnail(
|
|||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -575,6 +587,14 @@ async def update_course(
|
|||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
"""
|
||||
Update a course
|
||||
|
||||
SECURITY NOTES:
|
||||
- Requires course ownership (CREATOR, MAINTAINER) or admin role
|
||||
- Sensitive fields (public, open_to_contributors) require additional validation
|
||||
- Cannot change course access settings without proper permissions
|
||||
"""
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
|
|
@ -584,8 +604,46 @@ async def update_course(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
# SECURITY: Require course ownership or admin role for updating courses
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# SECURITY: Additional checks for sensitive access control fields
|
||||
sensitive_fields_updated = []
|
||||
|
||||
# Check if sensitive fields are being updated
|
||||
if course_object.public is not None:
|
||||
sensitive_fields_updated.append("public")
|
||||
if course_object.open_to_contributors is not None:
|
||||
sensitive_fields_updated.append("open_to_contributors")
|
||||
|
||||
# If sensitive fields are being updated, require additional validation
|
||||
if sensitive_fields_updated:
|
||||
# SECURITY: For sensitive access control changes, require CREATOR or MAINTAINER role
|
||||
# Check if user is course owner (CREATOR or MAINTAINER)
|
||||
statement = select(ResourceAuthor).where(
|
||||
ResourceAuthor.resource_uuid == course_uuid,
|
||||
ResourceAuthor.user_id == current_user.id
|
||||
)
|
||||
resource_author = db_session.exec(statement).first()
|
||||
|
||||
is_course_owner = False
|
||||
if resource_author:
|
||||
if ((resource_author.authorship == ResourceAuthorshipEnum.CREATOR) or
|
||||
(resource_author.authorship == ResourceAuthorshipEnum.MAINTAINER)) and \
|
||||
resource_author.authorship_status == ResourceAuthorshipStatusEnum.ACTIVE:
|
||||
is_course_owner = True
|
||||
|
||||
# Check if user has admin or maintainer role
|
||||
is_admin_or_maintainer = await authorization_verify_based_on_org_admin_status(
|
||||
request, current_user.id, "update", course_uuid, db_session
|
||||
)
|
||||
|
||||
# SECURITY: Only course owners (CREATOR, MAINTAINER) or admins can change access settings
|
||||
if not (is_course_owner or is_admin_or_maintainer):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"You must be the course owner (CREATOR or MAINTAINER) or have admin role to change access settings: {', '.join(sensitive_fields_updated)}",
|
||||
)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(course_object).items():
|
||||
|
|
@ -602,10 +660,10 @@ async def update_course(
|
|||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -643,7 +701,7 @@ async def delete_course(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Feature usage
|
||||
decrease_feature_usage("courses", course.org_id, db_session)
|
||||
|
|
@ -681,7 +739,7 @@ async def get_user_courses(
|
|||
return []
|
||||
|
||||
# Get courses with the extracted UUIDs
|
||||
statement = select(Course).where(Course.course_uuid.in_(course_uuids))
|
||||
statement = select(Course).where(Course.course_uuid.in_(course_uuids)) # type: ignore
|
||||
|
||||
# Apply pagination
|
||||
statement = statement.offset((page - 1) * limit).limit(limit)
|
||||
|
|
@ -738,39 +796,177 @@ async def get_user_courses(
|
|||
return result
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
async def get_course_user_rights(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
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)
|
||||
) -> dict:
|
||||
"""
|
||||
Get detailed user rights for a specific course.
|
||||
|
||||
This function returns comprehensive rights information that can be used
|
||||
by the UI to enable/disable features based on user permissions.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Returns rights based on course ownership and user roles
|
||||
- Includes both course-level and content-level permissions
|
||||
- Safe to expose to UI as it only returns permission information
|
||||
"""
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_uuid,
|
||||
db_session,
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Initialize rights object
|
||||
rights = {
|
||||
"course_uuid": course_uuid,
|
||||
"user_id": current_user.id,
|
||||
"is_anonymous": current_user.id == 0,
|
||||
"permissions": {
|
||||
"read": False,
|
||||
"create": False,
|
||||
"update": False,
|
||||
"delete": False,
|
||||
"create_content": False,
|
||||
"update_content": False,
|
||||
"delete_content": False,
|
||||
"manage_contributors": False,
|
||||
"manage_access": False,
|
||||
"grade_assignments": False,
|
||||
"mark_activities_done": False,
|
||||
"create_certifications": False,
|
||||
},
|
||||
"ownership": {
|
||||
"is_owner": False,
|
||||
"is_creator": False,
|
||||
"is_maintainer": False,
|
||||
"is_contributor": False,
|
||||
"authorship_status": None,
|
||||
},
|
||||
"roles": {
|
||||
"is_admin": False,
|
||||
"is_maintainer_role": False,
|
||||
"is_instructor": False,
|
||||
"is_user": False,
|
||||
}
|
||||
}
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
# Handle anonymous users
|
||||
if current_user.id == 0:
|
||||
# Anonymous users can only read public courses
|
||||
if course.public:
|
||||
rights["permissions"]["read"] = True
|
||||
return rights
|
||||
|
||||
# Check course ownership
|
||||
statement = select(ResourceAuthor).where(
|
||||
ResourceAuthor.resource_uuid == course_uuid,
|
||||
ResourceAuthor.user_id == current_user.id
|
||||
)
|
||||
resource_author = db_session.exec(statement).first()
|
||||
|
||||
if resource_author:
|
||||
rights["ownership"]["authorship_status"] = resource_author.authorship_status
|
||||
|
||||
if resource_author.authorship_status == ResourceAuthorshipStatusEnum.ACTIVE:
|
||||
if resource_author.authorship == ResourceAuthorshipEnum.CREATOR:
|
||||
rights["ownership"]["is_creator"] = True
|
||||
rights["ownership"]["is_owner"] = True
|
||||
elif resource_author.authorship == ResourceAuthorshipEnum.MAINTAINER:
|
||||
rights["ownership"]["is_maintainer"] = True
|
||||
rights["ownership"]["is_owner"] = True
|
||||
elif resource_author.authorship == ResourceAuthorshipEnum.CONTRIBUTOR:
|
||||
rights["ownership"]["is_contributor"] = True
|
||||
rights["ownership"]["is_owner"] = True
|
||||
|
||||
# Check user roles
|
||||
from src.security.rbac.rbac import authorization_verify_based_on_org_admin_status
|
||||
from src.security.rbac.rbac import authorization_verify_based_on_roles
|
||||
|
||||
# Check admin/maintainer role
|
||||
is_admin_or_maintainer = await authorization_verify_based_on_org_admin_status(
|
||||
request, current_user.id, "update", course_uuid, db_session
|
||||
)
|
||||
|
||||
if is_admin_or_maintainer:
|
||||
rights["roles"]["is_admin"] = True
|
||||
rights["roles"]["is_maintainer_role"] = True
|
||||
|
||||
# Check instructor role
|
||||
has_instructor_permissions = await authorization_verify_based_on_roles(
|
||||
request, current_user.id, "create", "course_x", db_session
|
||||
)
|
||||
|
||||
if has_instructor_permissions:
|
||||
rights["roles"]["is_instructor"] = True
|
||||
|
||||
# Check user role (basic permissions)
|
||||
has_user_permissions = await authorization_verify_based_on_roles(
|
||||
request, current_user.id, "read", course_uuid, db_session
|
||||
)
|
||||
|
||||
if has_user_permissions:
|
||||
rights["roles"]["is_user"] = True
|
||||
|
||||
# Determine permissions based on ownership and roles
|
||||
is_course_owner = rights["ownership"]["is_owner"]
|
||||
is_admin = rights["roles"]["is_admin"]
|
||||
is_maintainer_role = rights["roles"]["is_maintainer_role"]
|
||||
is_instructor = rights["roles"]["is_instructor"]
|
||||
|
||||
# READ permissions
|
||||
if course.public or is_course_owner or is_admin or is_maintainer_role or is_instructor or has_user_permissions:
|
||||
rights["permissions"]["read"] = True
|
||||
|
||||
# CREATE permissions (course creation)
|
||||
if is_instructor or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["create"] = True
|
||||
|
||||
# UPDATE permissions (course-level updates)
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["update"] = True
|
||||
|
||||
# DELETE permissions (course deletion)
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["delete"] = True
|
||||
|
||||
# CONTENT CREATION permissions (activities, assignments, chapters, etc.)
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["create_content"] = True
|
||||
|
||||
# CONTENT UPDATE permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["update_content"] = True
|
||||
|
||||
# CONTENT DELETE permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["delete_content"] = True
|
||||
|
||||
# CONTRIBUTOR MANAGEMENT permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["manage_contributors"] = True
|
||||
|
||||
# ACCESS MANAGEMENT permissions (public, open_to_contributors)
|
||||
if (rights["ownership"]["is_creator"] or rights["ownership"]["is_maintainer"] or
|
||||
is_admin or is_maintainer_role):
|
||||
rights["permissions"]["manage_access"] = True
|
||||
|
||||
# GRADING permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["grade_assignments"] = True
|
||||
|
||||
# ACTIVITY MARKING permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["mark_activities_done"] = True
|
||||
|
||||
# CERTIFICATION permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["create_certifications"] = True
|
||||
|
||||
return rights
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from src.db.courses.course_updates import (
|
|||
from src.db.courses.courses import Course
|
||||
from src.db.organizations import Organization
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.services.courses.courses import rbac_check
|
||||
from src.security.courses_security import courses_rbac_check
|
||||
|
||||
|
||||
async def create_update(
|
||||
|
|
@ -41,7 +41,7 @@ async def create_update(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Generate UUID
|
||||
courseupdate_uuid = str(f"courseupdate_{uuid4()}")
|
||||
|
|
@ -81,7 +81,7 @@ async def update_update(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(
|
||||
await courses_rbac_check(
|
||||
request, update.courseupdate_uuid, current_user, "update", db_session
|
||||
)
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ async def delete_update(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(
|
||||
await courses_rbac_check(
|
||||
request, update.courseupdate_uuid, current_user, "delete", db_session
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from src.db.payments.payments_courses import PaymentsCourse
|
|||
from src.db.payments.payments_products import PaymentsProduct
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.users import PublicUser, AnonymousUser
|
||||
from src.services.courses.courses import rbac_check
|
||||
from src.security.courses_security import courses_rbac_check
|
||||
|
||||
async def link_course_to_product(
|
||||
request: Request,
|
||||
|
|
@ -22,7 +22,7 @@ async def link_course_to_product(
|
|||
raise HTTPException(status_code=404, detail="Course not found")
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Check if product exists
|
||||
statement = select(PaymentsProduct).where(
|
||||
|
|
@ -71,7 +71,7 @@ async def unlink_course_from_product(
|
|||
raise HTTPException(status_code=404, detail="Course not found")
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Find and delete the payment course link
|
||||
statement = select(PaymentsCourse).where(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue