feat: users management

This commit is contained in:
swve 2024-01-22 20:37:11 +01:00
parent a552300e15
commit 689625b0d5
22 changed files with 621 additions and 36 deletions

View file

@ -1,5 +1,8 @@
from typing import Optional
from pydantic import BaseModel
from sqlmodel import Field, SQLModel
from src.db.roles import RoleRead
from src.db.organization_config import OrganizationConfig
@ -32,3 +35,9 @@ class OrganizationRead(OrganizationBase):
config: Optional[OrganizationConfig | dict]
creation_date: str
update_date: str
class OrganizationUser(BaseModel):
from src.db.users import UserRead
user: UserRead
role: RoleRead

View file

@ -1,9 +1,8 @@
from typing import Optional
from pydantic import BaseModel, EmailStr
from sqlmodel import Field, SQLModel
from src.db.roles import RoleRead
from src.db.organizations import OrganizationRead
class UserBase(SQLModel):
@ -45,6 +44,7 @@ class PublicUser(UserRead):
class UserRoleWithOrg(BaseModel):
from src.db.organizations import OrganizationRead
role: RoleRead
org: OrganizationRead

View file

@ -8,6 +8,7 @@ from src.db.organizations import (
OrganizationCreate,
OrganizationRead,
OrganizationUpdate,
OrganizationUser,
)
from src.core.events.database import get_db_session
from src.security.auth import get_current_user
@ -17,9 +18,12 @@ from src.services.orgs.orgs import (
delete_org,
get_organization,
get_organization_by_slug,
get_organization_users,
get_orgs_by_user,
remove_user_from_org,
update_org,
update_org_logo,
update_user_role,
)
@ -69,6 +73,52 @@ async def api_get_org(
return await get_organization(request, org_id, db_session, current_user)
@router.get("/{org_id}/users")
async def api_get_org_users(
request: Request,
org_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> list[OrganizationUser]:
"""
Get single Org by ID
"""
return await get_organization_users(request, org_id, db_session, current_user)
@router.put("/{org_id}/users/{user_id}/role/{role_uuid}")
async def api_update_user_role(
request: Request,
org_id: str,
user_id: str,
role_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Update user role
"""
return await update_user_role(
request, org_id, user_id, role_uuid, db_session, current_user
)
@router.delete("/{org_id}/users/{user_id}")
async def api_remove_user_from_org(
request: Request,
org_id: int,
user_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Remove user from org
"""
return await remove_user_from_org(
request, org_id, user_id, db_session, current_user
)
@router.get("/slug/{org_slug}")
async def api_get_org_by_slug(
request: Request,

View file

@ -16,7 +16,7 @@ async def authorization_verify_if_element_is_public(
element_uuid: str,
action: Literal["read"],
db_session: Session,
):
):
element_nature = await check_element_type(element_uuid)
# Verifies if the element is public
if element_nature == ("courses" or "collections") and action == "read":
@ -106,6 +106,34 @@ async def authorization_verify_based_on_roles(
return False
async def authorization_verify_based_on_org_admin_status(
request: Request,
user_id: int,
action: Literal["read", "update", "delete", "create"],
element_uuid: str,
db_session: Session,
):
await check_element_type(element_uuid)
# Get user roles bound to an organization and standard roles
statement = (
select(Role)
.join(UserOrganization)
.where((UserOrganization.org_id == Role.org_id) | (Role.org_id == null()))
.where(UserOrganization.user_id == user_id)
)
user_roles_in_organization_and_standard_roles = db_session.exec(statement).all()
# Find in roles list if there is a role that matches users action for this type of element
for role in user_roles_in_organization_and_standard_roles:
role = Role.from_orm(role)
if role.id == 1 or role.id == 2:
return True
else:
return False
# Tested and working
async def authorization_verify_based_on_roles_and_authorship(
request: Request,

View file

@ -225,7 +225,7 @@ async def get_course_chapters(
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
await rbac_check(request, course.course_uuid, current_user, "read", db_session) # type: ignore
# Get activities for each chapter
for chapter in chapters:

View file

@ -213,7 +213,7 @@ async def update_course_thumbnail(
if thumbnail_file and thumbnail_file.filename:
name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
await upload_thumbnail(
thumbnail_file, name_in_disk, 'users', course.course_uuid
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid
)
# Update course

View file

@ -4,6 +4,7 @@ from datetime import datetime
from typing import Literal
from uuid import uuid4
from sqlmodel import Session, select
from src.db.roles import Role, RoleRead
from src.db.organization_config import (
AIConfig,
AIEnabledFeatures,
@ -15,16 +16,18 @@ from src.db.organization_config import (
OrganizationConfigBase,
)
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_based_on_org_admin_status,
authorization_verify_based_on_roles,
authorization_verify_if_user_is_anon,
)
from src.db.users import AnonymousUser, PublicUser
from src.db.users import AnonymousUser, PublicUser, User, UserRead
from src.db.user_organizations import UserOrganization
from src.db.organizations import (
Organization,
OrganizationCreate,
OrganizationRead,
OrganizationUpdate,
OrganizationUser,
)
from src.services.orgs.logos import upload_org_logo
from fastapi import HTTPException, UploadFile, status, Request
@ -66,6 +69,199 @@ async def get_organization(
return org
async def get_organization_users(
request: Request,
org_id: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
) -> list[OrganizationUser]:
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
statement = (
select(User)
.join(UserOrganization)
.join(Organization)
.where(Organization.id == org_id)
)
users = db_session.exec(statement)
users = users.all()
org_users_list = []
for user in users:
statement = select(UserOrganization).where(
UserOrganization.user_id == user.id, UserOrganization.org_id == org_id
)
result = db_session.exec(statement)
user_org = result.first()
if not user_org:
logging.error(f"User {user.id} not found")
# skip this user
continue
statement = select(Role).where(Role.id == user_org.role_id)
result = db_session.exec(statement)
role = result.first()
if not role:
logging.error(f"Role {user_org.role_id} not found")
# skip this user
continue
statement = select(User).where(User.id == user_org.user_id)
result = db_session.exec(statement)
user = result.first()
if not user:
logging.error(f"User {user_org.user_id} not found")
# skip this user
continue
user = UserRead.from_orm(user)
role = RoleRead.from_orm(role)
org_user = OrganizationUser(
user=user,
role=role,
)
org_users_list.append(org_user)
return org_users_list
async def remove_user_from_org(
request: Request,
org_id: int,
user_id: int,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
statement = select(UserOrganization).where(
UserOrganization.user_id == user_id, UserOrganization.org_id == org.id
)
result = db_session.exec(statement)
user_org = result.first()
if not user_org:
raise HTTPException(
status_code=404,
detail="User not found",
)
# Check if user is the last admin
statement = select(UserOrganization).where(
UserOrganization.org_id == org.id, UserOrganization.role_id == 1
)
result = db_session.exec(statement)
admins = result.all()
if len(admins) == 1 and admins[0].user_id == user_id:
raise HTTPException(
status_code=400,
detail="You can't remove the last admin of the organization",
)
db_session.delete(user_org)
db_session.commit()
return {"detail": "User removed from org"}
async def update_user_role(
request: Request,
org_id: str,
user_id: str,
role_uuid: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
# find role
statement = select(Role).where(Role.role_uuid == role_uuid)
result = db_session.exec(statement)
role = result.first()
if not role:
raise HTTPException(
status_code=404,
detail="Role not found",
)
role_id = role.id
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
statement = select(UserOrganization).where(
UserOrganization.user_id == user_id, UserOrganization.org_id == org.id
)
result = db_session.exec(statement)
user_org = result.first()
if not user_org:
raise HTTPException(
status_code=404,
detail="User not found",
)
if role_id is not None:
user_org.role_id = role_id
db_session.add(user_org)
db_session.commit()
db_session.refresh(user_org)
return {"detail": "User role updated"}
async def get_organization_by_slug(
request: Request,
org_slug: str,
@ -443,7 +639,7 @@ async def get_orgs_by_user(
async def rbac_check(
request: Request,
org_id: str,
org_uuid: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
@ -455,8 +651,12 @@ async def rbac_check(
else:
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, org_id, db_session
await authorization_verify_based_on_roles(
request, current_user.id, action, org_uuid, db_session
)
await authorization_verify_based_on_org_admin_status(
request, current_user.id, action, org_uuid, db_session
)

View file

@ -5,7 +5,7 @@ async def upload_avatar(avatar_file, name_in_disk, user_uuid):
contents = avatar_file.file.read()
try:
await upload_content(
f"avatars",
"avatars",
"users",
user_uuid,
contents,