feat: init roles + authorship detection

This commit is contained in:
swve 2023-11-27 22:16:22 +01:00
parent 38288e8a57
commit 0595bfdb3f
16 changed files with 109 additions and 236 deletions

View file

@ -1,18 +0,0 @@
from typing import Optional
from sqlmodel import Field, SQLModel
from enum import Enum
class CourseAuthorshipEnum(str, Enum):
CREATOR = "CREATOR"
MAINTAINER = "MAINTAINER"
REPORTER = "REPORTER"
class CourseAuthor(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
course_id: int = Field(default=None, foreign_key="course.id")
user_id: int = Field(default=None, foreign_key="user.id")
authorship: CourseAuthorshipEnum = CourseAuthorshipEnum.CREATOR
creation_date: str
update_date: str

View file

@ -0,0 +1,20 @@
from enum import Enum
from typing import Optional, Union
from pydantic import BaseModel
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
class ResourceAuthorshipEnum(str, Enum):
CREATOR = "CREATOR"
MAINTAINER = "MAINTAINER"
REPORTER = "REPORTER"
class ResourceAuthor(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
resource_uuid: str
user_id: int = Field(default=None, foreign_key="user.id")
authorship: ResourceAuthorshipEnum = ResourceAuthorshipEnum.CREATOR
creation_date: str = ""
update_date: str = ""

View file

@ -36,7 +36,7 @@ class UserRead(UserBase):
class PublicUser(UserRead): class PublicUser(UserRead):
pass pass
class AnonymousUser(UserRead): class AnonymousUser(SQLModel):
id: str = "anonymous" id: str = "anonymous"
username: str = "anonymous" username: str = "anonymous"

View file

@ -4,7 +4,7 @@ from src.core.events.database import get_db_session
from src.db.roles import RoleCreate, RoleUpdate from src.db.roles import RoleCreate, RoleUpdate
from src.security.auth import get_current_user from src.security.auth import get_current_user
from src.services.roles.roles import create_role, delete_role, read_role, update_role from src.services.roles.roles import create_role, delete_role, read_role, update_role
from src.services.users.schemas.users import PublicUser from src.db.users import PublicUser
router = APIRouter() router = APIRouter()

View file

@ -1,6 +1,6 @@
from sqlmodel import Session from sqlmodel import Session
from src.core.events.database import get_db_session from src.core.events.database import get_db_session
from src.db.users import User, UserRead from src.db.users import AnonymousUser, User, UserRead
from src.services.users.users import security_get_user from src.services.users.users import security_get_user
from config.config import get_learnhouse_config from config.config import get_learnhouse_config
from pydantic import BaseModel from pydantic import BaseModel
@ -9,7 +9,6 @@ from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt from jose import JWTError, jwt
from datetime import datetime, timedelta from datetime import datetime, timedelta
from src.services.dev.dev import isDevModeEnabled from src.services.dev.dev import isDevModeEnabled
from src.services.users.schemas.users import AnonymousUser, PublicUser
from src.services.users.users import security_verify_password from src.services.users.users import security_verify_password
from src.security.security import ALGORITHM, SECRET_KEY from src.security.security import ALGORITHM, SECRET_KEY
from fastapi_jwt_auth import AuthJWT from fastapi_jwt_auth import AuthJWT
@ -100,6 +99,6 @@ async def get_current_user(
return AnonymousUser() return AnonymousUser()
async def non_public_endpoint(current_user: PublicUser): async def non_public_endpoint(current_user: UserRead | AnonymousUser):
if isinstance(current_user, AnonymousUser): if isinstance(current_user, AnonymousUser):
raise HTTPException(status_code=401, detail="Not authenticated") raise HTTPException(status_code=401, detail="Not authenticated")

View file

@ -1,17 +1,24 @@
from math import e
from typing import Literal from typing import Literal
from fastapi import HTTPException, status, Request from fastapi import HTTPException, status, Request
from src.security.rbac.utils import check_element_type, get_id_identifier_of_element from sqlalchemy import func, null, or_
from src.services.roles.schemas.roles import RoleInDB from sqlmodel import Session, select
from src.services.users.schemas.users import UserRolesInOrganization from src.db.collections import Collection
from src.db.courses import Course
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.roles import Role
from src.db.user_organizations import UserOrganization
from src.security.rbac.utils import check_element_type
async def authorization_verify_if_element_is_public( async def authorization_verify_if_element_is_public(
request, request,
element_id: str, element_uuid: str,
user_id: str, user_id: str,
action: Literal["read"], action: Literal["read"],
db_session: Session,
): ):
element_nature = await check_element_type(element_id) element_nature = await check_element_type(element_uuid)
# Verifies if the element is public # Verifies if the element is public
if ( if (
@ -20,10 +27,12 @@ async def authorization_verify_if_element_is_public(
and user_id == "anonymous" and user_id == "anonymous"
): ):
if element_nature == "courses": if element_nature == "courses":
courses = request.app.db["courses"] statement = select(Course).where(
course = await courses.find_one({"course_id": element_id}) Course.public == True, Course.course_uuid == element_uuid
)
course = db_session.exec(statement).first()
if course["public"]: if course:
return True return True
else: else:
raise HTTPException( raise HTTPException(
@ -32,10 +41,12 @@ async def authorization_verify_if_element_is_public(
) )
if element_nature == "collections": if element_nature == "collections":
collections = request.app.db["collections"] statement = select(Collection).where(
collection = await collections.find_one({"collection_id": element_id}) Collection.public == True, Collection.collection_uuid == element_uuid
)
collection = db_session.exec(statement).first()
if collection["public"]: if collection:
return True return True
else: else:
raise HTTPException( raise HTTPException(
@ -53,67 +64,65 @@ async def authorization_verify_if_user_is_author(
request, request,
user_id: str, user_id: str,
action: Literal["read", "update", "delete", "create"], action: Literal["read", "update", "delete", "create"],
element_id: str, element_uuid: str,
db_session: Session,
): ):
if action == "update" or "delete" or "read": if action == "update" or "delete" or "read":
element_nature = await check_element_type(element_id) statement = select(ResourceAuthor).where(
elements = request.app.db[element_nature] ResourceAuthor.resource_uuid == element_uuid
element_identifier = await get_id_identifier_of_element(element_id) )
element = await elements.find_one({element_identifier: element_id}) resource_author = db_session.exec(statement).first()
if user_id in element["authors"]:
if resource_author:
if resource_author.user_id == user_id:
if (resource_author.authorship == ResourceAuthorshipEnum.CREATOR) or (
resource_author.authorship == ResourceAuthorshipEnum.MAINTAINER
):
return True return True
else: else:
return False raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights (authorship) : You don't have the right to perform this action",
)
else: else:
return False raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Wrong action (create)",
)
async def authorization_verify_based_on_roles( async def authorization_verify_based_on_roles(
request: Request, request: Request,
user_id: str, user_id: str,
action: Literal["read", "update", "delete", "create"], action: Literal["read", "update", "delete", "create"],
roles_list: list[UserRolesInOrganization], element_uuid: str,
element_id: str, db_session: Session,
): ):
element_type = await check_element_type(element_id) element_type = await check_element_type(element_uuid)
element = request.app.db[element_type]
roles = request.app.db["roles"]
# Get the element # Get user roles bound to an organization and standard roles
element_identifier = await get_id_identifier_of_element(element_id) statement = (
element = await element.find_one({element_identifier: element_id}) select(Role)
.join(UserOrganization)
.where(UserOrganization.user_id == user_id)
.where((UserOrganization.id == Role.org_id) | (UserOrganization.id == null))
)
# Get the roles of the user user_roles_in_organization_and_standard_roles = db_session.exec(statement).all()
roles_id_list = [role["role_id"] for role in roles_list]
roles = await roles.find({"role_id": {"$in": roles_id_list}}).to_list(length=100)
async def checkRoles(): # Find in roles list if there is a role that matches users action for this type of element
# Check Roles for role in user_roles_in_organization_and_standard_roles:
for role in roles: role = Role.from_orm(role)
role = RoleInDB(**role) if role.rights:
if role.elements[element_type][f"action_{action}"] is True: rights = role.rights
if rights[element_type][f"action_{action}"] is True:
return True return True
else: else:
return False return False
async def checkOrgRoles():
# Check Org Roles
users = request.app.db["users"]
user = await users.find_one({"user_id": user_id})
if element is not None:
for org in user["orgs"]:
if org["org_id"] == element["org_id"]:
if org["org_role"] == "owner" or org["org_role"] == "editor":
return True
else:
return False
if await checkRoles() or await checkOrgRoles():
return True
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="User rights (roless) : You don't have the right to perform this action", detail="User rights (roles) : You don't have the right to perform this action",
) )
@ -121,15 +130,15 @@ async def authorization_verify_based_on_roles_and_authorship(
request: Request, request: Request,
user_id: str, user_id: str,
action: Literal["read", "update", "delete", "create"], action: Literal["read", "update", "delete", "create"],
roles_list: list[UserRolesInOrganization], element_uuid: str,
element_id: str, db_session: Session,
): ):
isAuthor = await authorization_verify_if_user_is_author( isAuthor = await authorization_verify_if_user_is_author(
request, user_id, action, element_id request, user_id, action, element_uuid, db_session
) )
isRole = await authorization_verify_based_on_roles( isRole = await authorization_verify_based_on_roles(
request, user_id, action, roles_list, element_id request, user_id, action, element_uuid, db_session
) )
if isAuthor or isRole: if isAuthor or isRole:

View file

@ -1,8 +1,10 @@
import json import json
import resource
from typing import Literal from typing import Literal
from uuid import uuid4 from uuid import uuid4
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.course_authors import CourseAuthor, CourseAuthorshipEnum from src import db
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.users import PublicUser, AnonymousUser from src.db.users import PublicUser, AnonymousUser
from src.db.courses import Course, CourseCreate, CourseRead, CourseUpdate from src.db.courses import Course, CourseCreate, CourseRead, CourseUpdate
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
@ -60,7 +62,7 @@ async def create_course(
# Complete course object # Complete course object
course.org_id = course.org_id course.org_id = course.org_id
course.course_uuid = str(uuid4()) course.course_uuid = str(f"course_{uuid4()}")
course.creation_date = str(datetime.now()) course.creation_date = str(datetime.now())
course.update_date = str(datetime.now()) course.update_date = str(datetime.now())
@ -76,18 +78,18 @@ async def create_course(
db_session.refresh(course) db_session.refresh(course)
# Make the user the creator of the course # Make the user the creator of the course
course_author = CourseAuthor( resource_author = ResourceAuthor(
course_id=course.id if course.id else 0, resource_uuid=course.course_uuid,
user_id=current_user.id, user_id=current_user.id,
authorship=CourseAuthorshipEnum.CREATOR, authorship=ResourceAuthorshipEnum.CREATOR,
creation_date=str(datetime.now()), creation_date=str(datetime.now()),
update_date=str(datetime.now()), update_date=str(datetime.now()),
) )
# Insert course author # Insert course author
db_session.add(course_author) db_session.add(resource_author)
db_session.commit() db_session.commit()
db_session.refresh(course_author) db_session.refresh(resource_author)
return CourseRead.from_orm(course) return CourseRead.from_orm(course)
@ -241,26 +243,23 @@ async def verify_rights(
course_id: str, course_id: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"], action: Literal["create", "read", "update", "delete"],
db_session: Session,
): ):
if action == "read": if action == "read":
if current_user.id == "anonymous": if current_user.id == "anonymous":
await authorization_verify_if_element_is_public( await authorization_verify_if_element_is_public(
request, course_id, str(current_user.id), action request, course_id, str(current_user.id), action, db_session
) )
else: else:
users = request.app.db["users"]
user = await users.find_one({"user_id": str(current_user.id)})
await authorization_verify_based_on_roles_and_authorship( await authorization_verify_based_on_roles_and_authorship(
request, request,
str(current_user.id), str(current_user.id),
action, action,
user["roles"],
course_id, course_id,
db_session,
) )
else: else:
users = request.app.db["users"]
user = await users.find_one({"user_id": str(current_user.id)})
await authorization_verify_if_user_is_anon(str(current_user.id)) await authorization_verify_if_user_is_anon(str(current_user.id))
@ -268,8 +267,8 @@ async def verify_rights(
request, request,
str(current_user.id), str(current_user.id),
action, action,
user["roles"],
course_id, course_id,
db_session,
) )

View file

@ -214,7 +214,10 @@ async def get_orgs_by_user(
statement = ( statement = (
select(Organization) select(Organization)
.join(UserOrganization) .join(UserOrganization)
.where(Organization.id == UserOrganization.org_id) .where(
Organization.id == UserOrganization.org_id,
UserOrganization.user_id == user_id,
)
) )
result = db_session.exec(statement) result = db_session.exec(statement)

View file

@ -1,28 +0,0 @@
from typing import Optional
from pydantic import BaseModel
#### Classes ####################################################
class Organization(BaseModel):
name: str
description: str
email: str
slug: str
logo: Optional[str]
default: Optional[bool] = False
class OrganizationInDB(Organization):
org_id: str
class PublicOrganization(Organization):
name: str
description: str
email: str
slug: str
org_id: str
def __getitem__(self, item):
return getattr(self, item)

View file

@ -1,7 +1,7 @@
from uuid import uuid4 from uuid import uuid4
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.users import PublicUser
from src.db.roles import Role, RoleCreate, RoleUpdate from src.db.roles import Role, RoleCreate, RoleUpdate
from src.services.users.schemas.users import PublicUser
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from datetime import datetime from datetime import datetime

View file

@ -1,41 +0,0 @@
from typing import Literal
from pydantic import BaseModel
# Database Models
class Permission(BaseModel):
action_create: bool
action_read: bool
action_update: bool
action_delete: bool
def __getitem__(self, item):
return getattr(self, item)
class Elements(BaseModel):
courses: Permission
users: Permission
houses: Permission
collections: Permission
organizations: Permission
coursechapters: Permission
activities: Permission
def __getitem__(self, item):
return getattr(self, item)
class Role(BaseModel):
name: str
description: str
elements : Elements
org_id: str | Literal["*"]
class RoleInDB(Role):
role_id: str
created_at: str
updated_at: str

View file

@ -1,70 +0,0 @@
from typing import Literal
from pydantic import BaseModel
class UserOrganization(BaseModel):
org_id: str
org_role: Literal['owner', 'editor', 'member']
def __getitem__(self, item):
return getattr(self, item)
class UserRolesInOrganization(BaseModel):
org_id: str
role_id: str
def __getitem__(self, item):
return getattr(self, item)
class User(BaseModel):
username: str
email: str
full_name: str | None = None
avatar_url: str | None = None
bio: str | None = None
class UserWithPassword(User):
password: str
class UserInDB(User):
user_id: str
password: str
verified: bool | None = False
disabled: bool | None = False
orgs: list[UserOrganization] = []
roles: list[UserRolesInOrganization] = []
creation_date: str
update_date: str
def __getitem__(self, item):
return getattr(self, item)
class PublicUser(User):
user_id: str
orgs: list[UserOrganization] = []
roles: list[UserRolesInOrganization] = []
creation_date: str
update_date: str
class AnonymousUser(BaseModel):
user_id: str = "anonymous"
username: str = "anonymous"
roles: list[UserRolesInOrganization] = [
UserRolesInOrganization(org_id="anonymous", role_id="role_anonymous")
]
# Forms ####################################################
class PasswordChangeForm(BaseModel):
old_password: str
new_password: str

View file

@ -4,6 +4,7 @@ from fastapi import HTTPException, Request, status
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.organizations import Organization from src.db.organizations import Organization
from src.db.users import ( from src.db.users import (
PublicUser,
User, User,
UserCreate, UserCreate,
UserRead, UserRead,
@ -12,7 +13,6 @@ from src.db.users import (
) )
from src.db.user_organizations import UserOrganization from src.db.user_organizations import UserOrganization
from src.security.security import security_hash_password, security_verify_password from src.security.security import security_hash_password, security_verify_password
from src.services.users.schemas.users import PublicUser
async def create_user( async def create_user(