diff --git a/pyproject.toml b/pyproject.toml index 968c012..8ce7e07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,21 @@ commit_version_number = true tag_commit = true changelog_file = "CHANGELOG.md" build_command = "python -m pip install build && python -m build" +allowed_tags = [ + "build", + "chore", + "ci", + "docs", + "feat", + "fix", + "perf", + "style", + "refactor", + "test", + "revert" +] +minor_tags = ["feat", "build"] +patch_tags = ["chore", "ci", "fix", "perf", "style", "refactor", "test", "revert"] [tool.flake8] max-line-length = 120 diff --git a/src/unipoll_api/__init__.py b/src/unipoll_api/__init__.py index e69de29..70be8a6 100644 --- a/src/unipoll_api/__init__.py +++ b/src/unipoll_api/__init__.py @@ -0,0 +1,7 @@ +from . import account_manager as AccountManager # noqa: F401 +from . import app as App # noqa: F401 +from . import config as Config # noqa: F401 +from . import dependencies as Dependencies # noqa: F401 +from . import documents as Documents # noqa: F401 +from . import mongo_db as MongoDB # noqa: F401 +from . import websocket_manager as WebsocketManager # noqa: F401 diff --git a/src/unipoll_api/account_manager.py b/src/unipoll_api/account_manager.py index fc49b6a..3e8bd04 100644 --- a/src/unipoll_api/account_manager.py +++ b/src/unipoll_api/account_manager.py @@ -3,14 +3,11 @@ from beanie import PydanticObjectId from fastapi import Depends, Request from fastapi_users import BaseUserManager, FastAPIUsers -from fastapi_users.authentication import (AuthenticationBackend, - CookieTransport) +from fastapi_users.authentication import AuthenticationBackend, CookieTransport from unipoll_api.utils.auth_transport import BearerTransport from unipoll_api.config import get_settings from fastapi_users_db_beanie import BeanieUserDatabase, ObjectIDIDMixin -# from fastapi_users_db_beanie.access_token import BeanieAccessTokenDatabase, BeanieBaseAccessToken from unipoll_api.utils.token_db import BeanieAccessTokenDatabase -# from fastapi_users.authentication.strategy.db import AccessTokenDatabase, DatabaseStrategy from unipoll_api.utils.auth_strategy import DatabaseStrategy from unipoll_api.documents import Account, AccessToken from unipoll_api.utils import colored_dbg @@ -83,4 +80,4 @@ def get_database_strategy(access_token_db=Depends(get_access_token_db)) -> Datab fastapi_users = FastAPIUsers[Account, PydanticObjectId](get_user_manager, [jwt_backend, cookie_backend]) # type: ignore get_current_active_user = fastapi_users.current_user(active=True) -current_active_user: ContextVar = ContextVar("current_active_user") +active_user: ContextVar = ContextVar("active_user") diff --git a/src/unipoll_api/actions/__init__.py b/src/unipoll_api/actions/__init__.py index e69de29..e6eebe2 100644 --- a/src/unipoll_api/actions/__init__.py +++ b/src/unipoll_api/actions/__init__.py @@ -0,0 +1,6 @@ +from . import account as AccountActions # noqa: F401 +from . import group as GroupActions # noqa: F401 +from . import poll as PollActions # noqa: F401 +from . import authentication as AuthActions # noqa: F401 +from . import workspace as WorkspaceActions # noqa: F401 +from . import permissions as PermissionsActions # noqa: F401 diff --git a/src/unipoll_api/actions/account.py b/src/unipoll_api/actions/account.py index b81a2de..f2f9699 100644 --- a/src/unipoll_api/actions/account.py +++ b/src/unipoll_api/actions/account.py @@ -1,34 +1,33 @@ -# Account actions -from unipoll_api import documents as Documents -from unipoll_api.account_manager import current_active_user -from unipoll_api.exceptions import account as AccountExceptions +from unipoll_api.documents import Account, Workspace, Group, AccessToken +from unipoll_api.account_manager import active_user +from unipoll_api.exceptions import AccountExceptions # Delete account -async def delete_account(account: Documents.Account | None = None) -> None: +async def delete_account(account: Account | None = None) -> None: if not account: - account = current_active_user.get() + account = active_user.get() # Delete account - await Documents.Account.delete(account) + await Account.delete(account) # Delete all policies associated with account # BUG: This doesn't work due to type mismatch - # await Documents.Policy.find({"policy_holder": account}).delete() # type: ignore + # await Policy.find({"policy_holder": account}).delete() # type: ignore # Remove account from all workspaces - workspaces = await Documents.Workspace.find(Documents.Workspace.members.id == account.id).to_list() # type: ignore + workspaces = await Workspace.find(Workspace.members.id == account.id).to_list() # type: ignore for workspace in workspaces: await workspace.remove_member(account) # type: ignore # Remove account from all groups - groups = await Documents.Group.find(Documents.Group.members.id == account.id).to_list() # type: ignore + groups = await Group.find(Group.members.id == account.id).to_list() # type: ignore for group in groups: await group.remove_member(account) # type: ignore # Check if account was deleted - if await Documents.Account.get(account.id): # type: ignore + if await Account.get(account.id): # type: ignore raise AccountExceptions.ErrorWhileDeleting(account.id) # type: ignore # Delete access tokens associated with account - await Documents.AccessToken.find(Documents.AccessToken.user_id == account.id).delete() # type: ignore + await AccessToken.find(AccessToken.user_id == account.id).delete() # type: ignore diff --git a/src/unipoll_api/actions/authentication.py b/src/unipoll_api/actions/authentication.py index 8235fd3..bd399e5 100644 --- a/src/unipoll_api/actions/authentication.py +++ b/src/unipoll_api/actions/authentication.py @@ -5,8 +5,7 @@ from fastapi import Depends from unipoll_api.documents import Account -from unipoll_api.exceptions import authentication as AuthExceptions -from unipoll_api.exceptions import account as AccountExceptions +from unipoll_api.exceptions import AuthExceptions, AccountExceptions from unipoll_api.utils import colored_dbg as Debug from unipoll_api.account_manager import jwt_backend, get_database_strategy, get_access_token_db diff --git a/src/unipoll_api/actions/group.py b/src/unipoll_api/actions/group.py index 0b586f6..f8eb664 100644 --- a/src/unipoll_api/actions/group.py +++ b/src/unipoll_api/actions/group.py @@ -1,17 +1,10 @@ from beanie import DeleteRules from beanie.operators import In -from unipoll_api.account_manager import current_active_user +from unipoll_api import AccountManager from unipoll_api.documents import Policy, ResourceID, Workspace, Group, Account -from unipoll_api.schemas import account as AccountSchemas -from unipoll_api.schemas import group as GroupSchemas -from unipoll_api.schemas import policy as PolicySchemas -# from unipoll_api.schemas import workspace as WorkspaceSchema -from unipoll_api.schemas import member as MemberSchemas -from unipoll_api.exceptions import account as AccountExceptions -from unipoll_api.exceptions import group as GroupExceptions -from unipoll_api.exceptions import workspace as WorkspaceExceptions -from unipoll_api.exceptions import resource as GenericExceptions -from unipoll_api.exceptions import policy as PolicyExceptions +from unipoll_api.schemas import AccountSchemas, GroupSchemas, MemberSchemas, PolicySchemas, WorkspaceSchemas +from unipoll_api.exceptions import (AccountExceptions, GroupExceptions, PolicyExceptions, + ResourceExceptions, WorkspaceExceptions) from unipoll_api.utils import permissions as Permissions @@ -28,8 +21,34 @@ # Get group -async def get_group(group: Group) -> GroupSchemas.Group: - return GroupSchemas.Group(**group.dict()) +async def get_group(group: Group, include_members: bool = False, include_policies: bool = False) -> GroupSchemas.Group: + members = None + policies = None + + account = AccountManager.active_user.get() + # Get the permissions(allowed actions) of the current user + permissions = await Permissions.get_all_permissions(group, account) + # Fetch the resources if the user has the required permissions + if include_members: + req_permissions = Permissions.GroupPermissions["get_group_members"] # type: ignore + if Permissions.check_permission(permissions, req_permissions): + members = (await get_group_members(group)).members + if include_policies: + req_permissions = Permissions.GroupPermissions["get_group_policies"] # type: ignore + if Permissions.check_permission(permissions, req_permissions): + policies = (await get_group_policies(group)).policies + + workspace = WorkspaceSchemas.Workspace(**group.workspace.dict(exclude={"members", # type: ignore + "policies", + "groups"})) + + # Return the workspace with the fetched resources + return GroupSchemas.Group(id=group.id, + name=group.name, + description=group.description, + workspace=workspace, + members=members, + policies=policies) # Update a group @@ -109,10 +128,10 @@ async def remove_group_member(group: Group, account_id: ResourceID | None): if not account: raise AccountExceptions.AccountNotFound(account_id) else: - account = current_active_user.get() + account = AccountManager.active_user.get() # Check if the account exists if not account: - raise GenericExceptions.InternalServerError("remove_group_member() -> Account not found") + raise ResourceExceptions.InternalServerError("remove_group_member() -> Account not found") # Check if account is a member of the group if account.id not in [ResourceID(member.ref.id) for member in group.members]: raise GroupExceptions.UserNotMember(group, account) @@ -135,10 +154,10 @@ async def get_group_policies(group: Group) -> PolicySchemas.PolicyList: elif policy.policy_holder_type == 'group': policy_holder = await Group.get(policy.policy_holder.ref.id) else: - raise GenericExceptions.InternalServerError("Invalid policy_holder_type") + raise ResourceExceptions.InternalServerError("Invalid policy_holder_type") if not policy_holder: # TODO: Replace with custom exception - raise GenericExceptions.InternalServerError("get_group_policies() => Policy holder not found") + raise ResourceExceptions.InternalServerError("get_group_policies() => Policy holder not found") # Convert the policy_holder to a Member schema policy_holder = MemberSchemas.Member(**policy_holder.dict()) # type: ignore policy_list.append(PolicySchemas.PolicyShort(id=policy.id, @@ -157,10 +176,10 @@ async def get_group_policy(group: Group, account_id: ResourceID | None): if not account: raise AccountExceptions.AccountNotFound(account_id) else: - account = current_active_user.get() + account = AccountManager.active_user.get() if not account: - raise GenericExceptions.InternalServerError("get_group_policy() => Account not found") + raise ResourceExceptions.InternalServerError("get_group_policy() => Account not found") # Check if account is a member of the group # if account.id not in [member.id for member in group.members]: @@ -191,10 +210,10 @@ async def set_group_policy(group: Group, if not account: raise AccountExceptions.AccountNotFound(input_data.account_id) else: - account = current_active_user.get() + account = AccountManager.active_user.get() # Make sure the account is loaded if not account: - raise GenericExceptions.InternalServerError("set_group_policy() => Account not found") + raise ResourceExceptions.InternalServerError("set_group_policy() => Account not found") try: # Find the policy for the account # NOTE: To set a policy for a user, the user must be a member of the group, therefore the policy must exist @@ -205,14 +224,14 @@ async def set_group_policy(group: Group, policy = p break except Exception as e: - raise GenericExceptions.InternalServerError(str(e)) + raise ResourceExceptions.InternalServerError(str(e)) # Calculate the new permission value new_permission_value = 0 for i in input_data.permissions: try: new_permission_value += Permissions.GroupPermissions[i].value # type: ignore except KeyError: - raise GenericExceptions.InvalidPermission(i) + raise ResourceExceptions.InvalidPermission(i) # Update the policy policy.permissions = Permissions.GroupPermissions(new_permission_value) # type: ignore await Policy.save(policy) diff --git a/src/unipoll_api/actions/permissions.py b/src/unipoll_api/actions/permissions.py index 3508a9c..bf68573 100644 --- a/src/unipoll_api/actions/permissions.py +++ b/src/unipoll_api/actions/permissions.py @@ -1,5 +1,5 @@ -from unipoll_api.schemas import policy as PolicySchemas -from unipoll_api.utils import permissions as Permissions +from unipoll_api.schemas import PolicySchemas +from unipoll_api.utils import Permissions # Get All Workspace Permissions diff --git a/src/unipoll_api/actions/poll.py b/src/unipoll_api/actions/poll.py index b56d592..0e55c3a 100644 --- a/src/unipoll_api/actions/poll.py +++ b/src/unipoll_api/actions/poll.py @@ -1,36 +1,29 @@ +from unipoll_api import AccountManager from unipoll_api.documents import Poll, Policy, Group, Account -from unipoll_api.schemas import poll as PollSchemas -from unipoll_api.schemas import question as QuestionSchemas -from unipoll_api.schemas import policy as PolicySchemas -from unipoll_api.schemas import member as MemberSchemas -from unipoll_api.schemas import workspace as WorkspaceSchema -from unipoll_api.utils import permissions as Permissions -from unipoll_api.exceptions import resource as GenericExceptions -from unipoll_api.account_manager import current_active_user +from unipoll_api.schemas import PollSchemas, QuestionSchemas, PolicySchemas, MemberSchemas, WorkspaceSchemas +from unipoll_api.utils import Permissions +from unipoll_api.exceptions import ResourceExceptions -async def get_poll(poll: Poll, include: list[str]) -> PollSchemas.PollResponse: - account = current_active_user.get() +async def get_poll(poll: Poll, + include_questions: bool = False, + include_policies: bool = False) -> PollSchemas.PollResponse: + account = AccountManager.active_user.get() questions = [] policies = None - if include: - # Get the permissions(allowed actions) of the current user - permissions = await Permissions.get_all_permissions(poll, account) - # If "all" is in the list, include all resources - if "all" in include: - include = ["policies", "questions"] - # Fetch the resources if the user has the required permissions - if "questions" in include: - req_permissions = Permissions.PollPermissions["get_poll_questions"] # type: ignore - if Permissions.check_permission(permissions, req_permissions) or poll.public: - questions = (await get_poll_questions(poll)).questions - if "policies" in include: - req_permissions = Permissions.PollPermissions["get_poll_policies"] # type: ignore - if Permissions.check_permission(permissions, req_permissions): - policies = (await get_poll_policies(poll)).policies + permissions = await Permissions.get_all_permissions(poll, account) + # Fetch the resources if the user has the required permissions + if include_questions: + req_permissions = Permissions.PollPermissions["get_poll_questions"] # type: ignore + if Permissions.check_permission(permissions, req_permissions) or poll.public: + questions = (await get_poll_questions(poll)).questions + if include_policies: + req_permissions = Permissions.PollPermissions["get_poll_policies"] # type: ignore + if Permissions.check_permission(permissions, req_permissions): + policies = (await get_poll_policies(poll)).policies - workspace = WorkspaceSchema.WorkspaceShort(**poll.workspace.dict()) # type: ignore + workspace = WorkspaceSchemas.WorkspaceShort(**poll.workspace.dict()) # type: ignore # Return the workspace with the fetched resources return PollSchemas.PollResponse(id=poll.id, @@ -65,10 +58,10 @@ async def get_poll_policies(poll: Poll) -> PolicySchemas.PolicyList: elif policy.policy_holder_type == 'group': policy_holder = await Group.get(policy.policy_holder.ref.id) else: - raise GenericExceptions.InternalServerError("Invalid policy_holder_type") + raise ResourceExceptions.InternalServerError("Invalid policy_holder_type") if not policy_holder: # TODO: Replace with custom exception - raise GenericExceptions.InternalServerError("get_poll_policies() => Policy holder not found") + raise ResourceExceptions.InternalServerError("get_poll_policies() => Policy holder not found") # Convert the policy_holder to a Member schema policy_holder = MemberSchemas.Member(**policy_holder.dict()) # type: ignore policy_list.append(PolicySchemas.PolicyShort(id=policy.id, @@ -94,7 +87,7 @@ async def update_poll(poll: Poll, data: PollSchemas.UpdatePollRequest) -> PollSc # Save the updated poll await Poll.save(poll) - return await get_poll(poll, []) + return await get_poll(poll, include_questions=True) async def delete_poll(poll: Poll): @@ -103,4 +96,4 @@ async def delete_poll(poll: Poll): # Check if the poll was deleted if await Poll.get(poll.id): - raise GenericExceptions.InternalServerError("Poll not deleted") + raise ResourceExceptions.InternalServerError("Poll not deleted") diff --git a/src/unipoll_api/actions/superuser.py b/src/unipoll_api/actions/superuser.py index c72ed38..b09e8e4 100644 --- a/src/unipoll_api/actions/superuser.py +++ b/src/unipoll_api/actions/superuser.py @@ -1,5 +1,5 @@ from unipoll_api.documents import Workspace -from unipoll_api.schemas import workspace as WorkspaceSchemas +from unipoll_api.schemas import WorkspaceSchemas # Get all workspaces diff --git a/src/unipoll_api/actions/workspace.py b/src/unipoll_api/actions/workspace.py index b15408a..27cc8ee 100644 --- a/src/unipoll_api/actions/workspace.py +++ b/src/unipoll_api/actions/workspace.py @@ -2,29 +2,17 @@ # from pydantic import EmailStr from beanie import WriteRules, DeleteRules from beanie.operators import In -from unipoll_api.account_manager import current_active_user +from unipoll_api import AccountManager from unipoll_api.documents import Group, ResourceID, Workspace, Account, Policy, Poll, create_link -from unipoll_api.utils import permissions as Permissions - -# Schemas -from unipoll_api.schemas import workspace as WorkspaceSchemas -from unipoll_api.schemas import group as GroupSchemas -from unipoll_api.schemas import policy as PolicySchemas -from unipoll_api.schemas import member as MemberSchemas -from unipoll_api.schemas import poll as PollSchemas - -# Exceptions -from unipoll_api.exceptions import workspace as WorkspaceExceptions -from unipoll_api.exceptions import account as AccountExceptions -from unipoll_api.exceptions import group as GroupExceptions -from unipoll_api.exceptions import resource as GenericExceptions -from unipoll_api.exceptions import policy as PolicyExceptions -from unipoll_api.exceptions import poll as PollExceptions +from unipoll_api.utils import Permissions +from unipoll_api.schemas import WorkspaceSchemas, GroupSchemas, PolicySchemas, MemberSchemas, PollSchemas +from unipoll_api.exceptions import (WorkspaceExceptions, AccountExceptions, GroupExceptions, ResourceExceptions, + PolicyExceptions, PollExceptions) # Get a list of workspaces where the account is a owner/member async def get_workspaces() -> WorkspaceSchemas.WorkspaceList: - account = current_active_user.get() + account = AccountManager.active_user.get() workspace_list = [] search_result = await Workspace.find(Workspace.members.id == account.id).to_list() # type: ignore @@ -39,7 +27,7 @@ async def get_workspaces() -> WorkspaceSchemas.WorkspaceList: # Create a new workspace with account as the owner async def create_workspace(input_data: WorkspaceSchemas.WorkspaceCreateInput) -> WorkspaceSchemas.WorkspaceCreateOutput: - account: Account = current_active_user.get() + account: Account = AccountManager.active_user.get() # Check if workspace name is unique if await Workspace.find_one({"name": input_data.name}): raise WorkspaceExceptions.NonUniqueName(input_data.name) @@ -68,8 +56,43 @@ async def create_workspace(input_data: WorkspaceSchemas.WorkspaceCreateInput) -> # Get a workspace -async def get_workspace(workspace: Workspace) -> Workspace: - return workspace +async def get_workspace(workspace: Workspace, + include_groups: bool = False, + include_policies: bool = False, + include_members: bool = False, + include_polls: bool = False) -> WorkspaceSchemas.Workspace: + groups = None + members = None + policies = None + polls = None + # Get the current active user + account = AccountManager.active_user.get() + # Get the permissions(allowed actions) of the current user + permissions = await Permissions.get_all_permissions(workspace, account) + if include_groups: + req_permissions = Permissions.WorkspacePermissions["get_groups"] # type: ignore + if Permissions.check_permission(permissions, req_permissions): + groups = (await get_groups(workspace)).groups + if include_members: + req_permissions = Permissions.WorkspacePermissions["get_workspace_members"] # type: ignore + if Permissions.check_permission(permissions, req_permissions): + members = (await get_workspace_members(workspace)).members + if include_policies: + req_permissions = Permissions.WorkspacePermissions["get_workspace_policies"] # type: ignore + if Permissions.check_permission(permissions, req_permissions): + policies = (await get_workspace_policies(workspace)).policies + if include_polls: + req_permissions = Permissions.WorkspacePermissions["get_polls"] # type: ignore + if Permissions.check_permission(permissions, req_permissions): + polls = (await get_polls(workspace)).polls + # Return the workspace with the fetched resources + return WorkspaceSchemas.Workspace(id=workspace.id, + name=workspace.name, + description=workspace.description, + groups=groups, + members=members, + policies=policies, + polls=polls) # Update a workspace @@ -138,7 +161,7 @@ async def remove_workspace_member(workspace: Workspace, account_id: ResourceID): if account_id: account = await Account.get(account_id) # type: ignore else: - account = current_active_user.get() + account = AccountManager.active_user.get() # Check if the account exists if not account: raise AccountExceptions.AccountNotFound(account_id) @@ -156,7 +179,7 @@ async def remove_workspace_member(workspace: Workspace, account_id: ResourceID): # Get a list of groups where the account is a member async def get_groups(workspace: Workspace) -> GroupSchemas.GroupList: # await workspace.fetch_link(Workspace.groups) - account = current_active_user.get() + account = AccountManager.active_user.get() group_list = [] # Convert the list of links to a list of @@ -174,7 +197,7 @@ async def get_groups(workspace: Workspace) -> GroupSchemas.GroupList: async def create_group(workspace: Workspace, input_data: GroupSchemas.GroupCreateInput) -> GroupSchemas.GroupCreateOutput: # await workspace.fetch_link(workspace.groups) - account = current_active_user.get() + account = AccountManager.active_user.get() # Check if group name is unique group: Group # For type hinting, until Link type is supported @@ -222,7 +245,7 @@ async def get_workspace_policies(workspace: Workspace) -> PolicySchemas.PolicyLi elif policy.policy_holder_type == 'group': policy_holder = await Group.get(policy.policy_holder.ref.id) else: - raise GenericExceptions.InternalServerError(str("Unknown policy_holder_type")) + raise ResourceExceptions.InternalServerError(str("Unknown policy_holder_type")) if not policy_holder: # TODO: Replace with a custom exception raise AccountExceptions.AccountNotFound(policy.policy_holder.ref.id) @@ -240,7 +263,7 @@ async def get_workspace_policies(workspace: Workspace) -> PolicySchemas.PolicyLi async def get_workspace_policy(workspace: Workspace, account_id: ResourceID | None = None) -> PolicySchemas.PolicyOutput: # Check if account_id is specified in request, if account_id is not specified, use the current user - account: Account = await Account.get(account_id) if account_id else current_active_user.get() # type: ignore + account: Account = await Account.get(account_id) if account_id else AccountManager.active_user.get() # type: ignore if not account and account_id: raise AccountExceptions.AccountNotFound(account_id) @@ -273,10 +296,10 @@ async def set_workspace_policy(workspace: Workspace, if not account: raise AccountExceptions.AccountNotFound(input_data.account_id) else: - account = current_active_user.get() + account = AccountManager.active_user.get() # Make sure the account is loaded if not account: - raise GenericExceptions.APIException(code=500, detail='Unknown error') # Should not happen + raise ResourceExceptions.APIException(code=500, detail='Unknown error') # Should not happen try: # Find the policy for the account @@ -292,7 +315,7 @@ async def set_workspace_policy(workspace: Workspace, # permissions=Permissions.WorkspacePermissions(0), # workspace=workspace) except Exception as e: - raise GenericExceptions.InternalServerError(str(e)) + raise ResourceExceptions.InternalServerError(str(e)) # Calculate the new permission value from request new_permission_value = 0 @@ -300,7 +323,7 @@ async def set_workspace_policy(workspace: Workspace, try: new_permission_value += Permissions.WorkspacePermissions[i].value # type: ignore except KeyError: - raise GenericExceptions.InvalidPermission(i) + raise ResourceExceptions.InvalidPermission(i) # Update permissions policy.permissions = Permissions.WorkspacePermissions(new_permission_value) # type: ignore await Policy.save(policy) diff --git a/src/unipoll_api/app.py b/src/unipoll_api/app.py index fc11ff3..fd60c24 100644 --- a/src/unipoll_api/app.py +++ b/src/unipoll_api/app.py @@ -4,7 +4,7 @@ from fastapi.middleware.cors import CORSMiddleware from beanie import init_beanie from unipoll_api.routes import router -from unipoll_api.mongo_db import mainDB, DOCUMENT_MODELS +from unipoll_api.mongo_db import mainDB, documentModels from unipoll_api.config import get_settings from unipoll_api.__version__ import version from unipoll_api.utils import cli_args, colored_dbg @@ -47,7 +47,7 @@ async def on_startup() -> None: await init_beanie( database=mainDB, - document_models=DOCUMENT_MODELS, # type: ignore + document_models=documentModels # type: ignore ) diff --git a/src/unipoll_api/dependencies.py b/src/unipoll_api/dependencies.py index 60d86b4..95b2207 100644 --- a/src/unipoll_api/dependencies.py +++ b/src/unipoll_api/dependencies.py @@ -1,13 +1,9 @@ from typing import Annotated from fastapi import Cookie, Depends, Query, Request, HTTPException, WebSocket -from unipoll_api.account_manager import current_active_user, get_current_active_user +from unipoll_api.account_manager import active_user, get_current_active_user from unipoll_api.documents import ResourceID, Workspace, Group, Account, Poll from unipoll_api.utils import permissions as Permissions -# Exceptions -from unipoll_api.exceptions import workspace as WorkspaceExceptions -from unipoll_api.exceptions import group as GroupExceptions -from unipoll_api.exceptions import account as AccountExceptions -from unipoll_api.exceptions import poll as PollExceptions +from unipoll_api.exceptions import WorkspaceExceptions, GroupExceptions, AccountExceptions, PollExceptions from unipoll_api.utils.path_operations import extract_action_from_path, extract_resourceID_from_path @@ -66,7 +62,7 @@ async def get_poll_model(poll_id: ResourceID) -> Poll: # Dependency to get a user by id and verify it exists async def set_active_user(user_account: Account = Depends(get_current_active_user)): - current_active_user.set(user_account) + active_user.set(user_account) return user_account diff --git a/src/unipoll_api/documents.py b/src/unipoll_api/documents.py index 7da9ebb..f64ec65 100644 --- a/src/unipoll_api/documents.py +++ b/src/unipoll_api/documents.py @@ -1,14 +1,12 @@ # from typing import ForwardRef, NewType, TypeAlias, Optional from typing import Literal from bson import DBRef -# from typing import ForwardRef, Optional from beanie import Document, WriteRules, after_event, Insert, Link, PydanticObjectId # BackLink from fastapi_users_db_beanie import BeanieBaseUser from pydantic import Field from unipoll_api.utils import colored_dbg as Debug from unipoll_api.utils.permissions import Permissions # WorkspacePermissions from unipoll_api.utils.token_db import BeanieBaseAccessToken -# from unipoll_api.schemas.question import Question # Create a link to the Document model diff --git a/src/unipoll_api/exceptions/__init__.py b/src/unipoll_api/exceptions/__init__.py index e69de29..26410b2 100644 --- a/src/unipoll_api/exceptions/__init__.py +++ b/src/unipoll_api/exceptions/__init__.py @@ -0,0 +1,7 @@ +from . import account as AccountExceptions # noqa: F401 +from . import authentication as AuthExceptions # noqa: F401 +from . import group as GroupExceptions # noqa: F401 +from . import policy as PolicyExceptions # noqa: F401 +from . import poll as PollExceptions # noqa: F401 +from . import resource as ResourceExceptions # noqa: F401 +from . import workspace as WorkspaceExceptions # noqa: F401 diff --git a/src/unipoll_api/exceptions/resource.py b/src/unipoll_api/exceptions/resource.py index ea56568..75eb2a7 100644 --- a/src/unipoll_api/exceptions/resource.py +++ b/src/unipoll_api/exceptions/resource.py @@ -1,7 +1,6 @@ from fastapi import status -from unipoll_api.documents import Account, Resource -from unipoll_api.utils import colored_dbg as Debug -from beanie import PydanticObjectId +from unipoll_api.documents import Account, Resource, ResourceID +from unipoll_api.utils import Debug class APIException(Exception): @@ -34,13 +33,13 @@ def __init__(self, resource: str, resource_name: str): class ResourceNotFound(APIException): - def __init__(self, resource: str, resource_id: PydanticObjectId): + def __init__(self, resource: str, resource_id: ResourceID): super().__init__(code=status.HTTP_404_NOT_FOUND, detail=f"{resource} #{resource_id} does not exist") class ErrorWhileDeleting(APIException): - def __init__(self, resource: str, resource_id: PydanticObjectId): + def __init__(self, resource: str, resource_id: ResourceID): super().__init__(code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error while deleting {resource} #{resource_id}") diff --git a/src/unipoll_api/mongo_db.py b/src/unipoll_api/mongo_db.py index 1bdfcd3..5b0e407 100644 --- a/src/unipoll_api/mongo_db.py +++ b/src/unipoll_api/mongo_db.py @@ -1,23 +1,21 @@ import motor.motor_asyncio # type: ignore from unipoll_api import documents as Documents -# import documents as Documents from unipoll_api.config import get_settings settings = get_settings() -DATABASE_URL = settings.mongodb_url client = motor.motor_asyncio.AsyncIOMotorClient( - DATABASE_URL, uuidRepresentation="standard" + settings.mongodb_url, uuidRepresentation="standard" ) mainDB = client.app -DOCUMENT_MODELS = [ +documentModels = [ Documents.AccessToken, Documents.Resource, Documents.Account, Documents.Group, Documents.Workspace, Documents.Policy, - Documents.Poll, + Documents.Poll ] diff --git a/src/unipoll_api/routes/__init__.py b/src/unipoll_api/routes/__init__.py index 34ae237..44607a0 100644 --- a/src/unipoll_api/routes/__init__.py +++ b/src/unipoll_api/routes/__init__.py @@ -10,7 +10,7 @@ from . import workspace as WorkspaceRoutes # Create main router -router = APIRouter() +router: APIRouter = APIRouter() # Add endpoints defined in the routes directory router.include_router(WorkspaceRoutes.open_router, diff --git a/src/unipoll_api/routes/account.py b/src/unipoll_api/routes/account.py index 131749e..0bc805b 100644 --- a/src/unipoll_api/routes/account.py +++ b/src/unipoll_api/routes/account.py @@ -1,14 +1,14 @@ from fastapi import APIRouter, status, HTTPException, Depends from unipoll_api.account_manager import fastapi_users -from unipoll_api.actions import account as AccountActions +from unipoll_api.actions import AccountActions from unipoll_api.exceptions.resource import APIException from unipoll_api.documents import Account from unipoll_api.dependencies import get_account -from unipoll_api.schemas import account as AccountSchemas +from unipoll_api.schemas import AccountSchemas # APIRouter creates path operations for user module -router = APIRouter() +router: APIRouter = APIRouter() @router.get("", diff --git a/src/unipoll_api/routes/authentication.py b/src/unipoll_api/routes/authentication.py index f7f5e17..57abbba 100644 --- a/src/unipoll_api/routes/authentication.py +++ b/src/unipoll_api/routes/authentication.py @@ -14,7 +14,7 @@ from unipoll_api.schemas import account as AccountSchemas from unipoll_api.exceptions.resource import APIException from unipoll_api.utils.token_db import BeanieAccessTokenDatabase -router = APIRouter() +router: APIRouter = APIRouter() login_responses: OpenAPIResponseType = { diff --git a/src/unipoll_api/routes/group.py b/src/unipoll_api/routes/group.py index ee7eb6e..741f7ac 100644 --- a/src/unipoll_api/routes/group.py +++ b/src/unipoll_api/routes/group.py @@ -2,21 +2,15 @@ from typing import Annotated, Literal from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, status from unipoll_api import dependencies as Dependencies -from unipoll_api.actions import group as GroupActions -from unipoll_api.actions import permissions as PermissionsActions +from unipoll_api.actions import GroupActions, PermissionsActions from unipoll_api.exceptions.resource import APIException -from unipoll_api.schemas import workspace as WorkspaceSchema -from unipoll_api.schemas import group as GroupSchemas -from unipoll_api.schemas import policy as PolicySchemas -from unipoll_api.schemas import member as MemberSchemas +from unipoll_api.schemas import GroupSchemas, PolicySchemas, MemberSchemas from unipoll_api.documents import Group, ResourceID -from unipoll_api.account_manager import current_active_user -from unipoll_api.utils import permissions as Permissions # APIRouter creates path operations for user module -open_router = APIRouter() -router = APIRouter(dependencies=[Depends(Dependencies.check_group_permission)]) +open_router: APIRouter = APIRouter() +router: APIRouter = APIRouter(dependencies=[Depends(Dependencies.check_group_permission)]) # Get all groups @@ -35,41 +29,18 @@ response_model_exclude_defaults=True, response_model_exclude_none=True) async def get_group(group: Group = Depends(Dependencies.get_group_model), - include: Annotated[query_params | None, Query()] = None - ): + include: Annotated[query_params | None, Query()] = None): try: - # await group.fetch_all_links() - account = current_active_user.get() - members = None - policies = None - + params = {} if include: - # Get the permissions(allowed actions) of the current user - permissions = await Permissions.get_all_permissions(group, account) - # If "all" is in the list, include all resources if "all" in include: - include = ["policies", "members"] - # Fetch the resources if the user has the required permissions - if "members" in include: - req_permissions = Permissions.GroupPermissions["get_group_members"] # type: ignore - if Permissions.check_permission(permissions, req_permissions): - members = (await GroupActions.get_group_members(group)).members - if "policies" in include: - req_permissions = Permissions.GroupPermissions["get_group_policies"] # type: ignore - if Permissions.check_permission(permissions, req_permissions): - policies = (await GroupActions.get_group_policies(group)).policies - - workspace = WorkspaceSchema.Workspace(**group.workspace.dict(exclude={"members", # type: ignore - "policies", - "groups"})) - - # Return the workspace with the fetched resources - return GroupSchemas.Group(id=group.id, - name=group.name, - description=group.description, - workspace=workspace, - members=members, - policies=policies) + params = {"include_members": True, "include_polls": True} + else: + if "members" in include: + params["include_members"] = True + if "policies" in include: + params["include_policies"] = True + return await GroupActions.get_group(group, **params) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) diff --git a/src/unipoll_api/routes/poll.py b/src/unipoll_api/routes/poll.py index bd5a116..f9431bf 100644 --- a/src/unipoll_api/routes/poll.py +++ b/src/unipoll_api/routes/poll.py @@ -5,13 +5,11 @@ from unipoll_api import dependencies as Dependencies from unipoll_api.documents import Poll from unipoll_api.exceptions.resource import APIException -from unipoll_api.schemas import poll as PollSchemas -from unipoll_api.schemas import question as QuestionSchemas -from unipoll_api.schemas import policy as PolicySchema -from unipoll_api.actions import poll as PollActions +from unipoll_api.actions import PollActions +from unipoll_api.schemas import PollSchemas, QuestionSchemas, PolicySchemas -open_router = APIRouter() -router = APIRouter(dependencies=[Depends(Dependencies.check_poll_permission)]) +open_router: APIRouter = APIRouter() +router: APIRouter = APIRouter(dependencies=[Depends(Dependencies.check_poll_permission)]) query_params = list[Literal["all", "questions", "policies"]] @@ -25,7 +23,16 @@ async def get_poll(poll: Poll = Depends(Dependencies.get_poll_model), include: Annotated[query_params | None, Query()] = None): try: - return await PollActions.get_poll(poll, list(include or [])) + params = {} + if include: + if "all" in include: + params = {"include_questions": True, "include_policies": True} + else: + if "questions" in include: + params = {"include_questions": True} + if "policies" in include: + params = {"include_policies": True} + return await PollActions.get_poll(poll, **params) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -69,7 +76,7 @@ async def get_questions(poll: Poll = Depends(Dependencies.get_poll_model), @router.get("/{poll_id}/policies", response_description="Policy list of a poll", - response_model=PolicySchema.PolicyList, + response_model=PolicySchemas.PolicyList, response_model_exclude_none=True) async def get_policies(poll: Poll = Depends(Dependencies.get_poll_model), include: Annotated[query_params | None, Query()] = None): diff --git a/src/unipoll_api/routes/websocket.py b/src/unipoll_api/routes/websocket.py index 974eef3..f51a5e5 100644 --- a/src/unipoll_api/routes/websocket.py +++ b/src/unipoll_api/routes/websocket.py @@ -1,13 +1,10 @@ # Handle WebSocket connections -# from typing import Annotated from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect -# from app.models.documents import Account from unipoll_api.websocket_manager import WebSocketManager from unipoll_api.dependencies import websocket_auth -# from app.account_manager import AccessToken -router = APIRouter() +router: APIRouter = APIRouter() # Create a connection manager to manage WebSocket connections manager = WebSocketManager() diff --git a/src/unipoll_api/routes/workspace.py b/src/unipoll_api/routes/workspace.py index 7ec1384..92159ca 100644 --- a/src/unipoll_api/routes/workspace.py +++ b/src/unipoll_api/routes/workspace.py @@ -1,22 +1,15 @@ # FastAPI from typing import Annotated, Literal from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, status -from unipoll_api.actions import workspace as WorkspaceActions -from unipoll_api.actions import permissions as PermissionsActions +from unipoll_api import dependencies as Dependencies +from unipoll_api.actions import WorkspaceActions, PermissionsActions from unipoll_api.exceptions.resource import APIException from unipoll_api.documents import Workspace, ResourceID -from unipoll_api.schemas import workspace as WorkspaceSchemas -from unipoll_api.schemas import policy as PolicySchemas -from unipoll_api.schemas import group as GroupSchemas -from unipoll_api.schemas import member as MemberSchemas -from unipoll_api.schemas import poll as PollSchemas -from unipoll_api import dependencies as Dependencies -from unipoll_api.utils import permissions as Permissions -from unipoll_api.account_manager import current_active_user +from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, GroupSchemas, MemberSchemas, PollSchemas # APIRouter creates path operations for user module -open_router = APIRouter() -router = APIRouter(dependencies=[Depends(Dependencies.check_workspace_permission)]) +open_router: APIRouter = APIRouter() +router: APIRouter = APIRouter(dependencies=[Depends(Dependencies.check_workspace_permission)]) # TODO: Move to open router to a separate file @@ -65,8 +58,7 @@ async def create_workspace(input_data: WorkspaceSchemas.WorkspaceCreateInput = B response_model_exclude_defaults=True, response_model_exclude_none=True) async def get_workspace(workspace: Workspace = Depends(Dependencies.get_workspace_model), - include: Annotated[query_params | None, Query()] = None - ) -> WorkspaceSchemas.Workspace: + include: Annotated[query_params | None, Query()] = None): """ ### Description: Endpoint to get a workspace with the given id. @@ -98,44 +90,23 @@ async def get_workspace(workspace: Workspace = Depends(Dependencies.get_workspac Returns a workspace with the given id. """ try: - # await workspace.fetch_all_links() - account = current_active_user.get() - groups = None - members = None - policies = None - polls = None - + params = {} if include: - # Get the permissions(allowed actions) of the current user - permissions = await Permissions.get_all_permissions(workspace, account) - # If "all" is in the list, include all resources if "all" in include: - include = ["policies", "groups", "members", "polls"] - # Fetch the resources if the user has the required permissions - if "groups" in include: - req_permissions = Permissions.WorkspacePermissions["get_groups"] # type: ignore - if Permissions.check_permission(permissions, req_permissions): - groups = (await WorkspaceActions.get_groups(workspace)).groups - if "members" in include: - req_permissions = Permissions.WorkspacePermissions["get_workspace_members"] # type: ignore - if Permissions.check_permission(permissions, req_permissions): - members = (await WorkspaceActions.get_workspace_members(workspace)).members - if "policies" in include: - req_permissions = Permissions.WorkspacePermissions["get_workspace_policies"] # type: ignore - if Permissions.check_permission(permissions, req_permissions): - policies = (await WorkspaceActions.get_workspace_policies(workspace)).policies - if "polls" in include: - req_permissions = Permissions.WorkspacePermissions["get_polls"] # type: ignore - if Permissions.check_permission(permissions, req_permissions): - polls = (await WorkspaceActions.get_polls(workspace)).polls - # Return the workspace with the fetched resources - return WorkspaceSchemas.Workspace(id=workspace.id, - name=workspace.name, - description=workspace.description, - groups=groups, - members=members, - policies=policies, - polls=polls) + params = {"include_groups": True, + "include_members": True, + "include_policies": True, + "include_polls": True} + else: + if "groups" in include: + params["include_groups"] = True + if "members" in include: + params["include_members"] = True + if "policies" in include: + params["include_policies"] = True + if "polls" in include: + params["include_polls"] = True + return await WorkspaceActions.get_workspace(workspace, **params) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) diff --git a/src/unipoll_api/schemas/__init__.py b/src/unipoll_api/schemas/__init__.py index e69de29..304d0d0 100644 --- a/src/unipoll_api/schemas/__init__.py +++ b/src/unipoll_api/schemas/__init__.py @@ -0,0 +1,8 @@ +from . import account as AccountSchemas # noqa: F401 +from . import authentication as AuthSchemas # noqa: F401 +from . import group as GroupSchemas # noqa: F401 +from . import member as MemberSchemas # noqa: F401 +from . import policy as PolicySchemas # noqa: F401 +from . import poll as PollSchemas # noqa: F401 +from . import question as QuestionSchemas # noqa: F401 +from . import workspace as WorkspaceSchemas # noqa: F401 diff --git a/src/unipoll_api/schemas/workspace.py b/src/unipoll_api/schemas/workspace.py index 7de3d7c..fc59cb6 100644 --- a/src/unipoll_api/schemas/workspace.py +++ b/src/unipoll_api/schemas/workspace.py @@ -1,4 +1,3 @@ -# from bson import DBRef from pydantic import BaseModel, Field from typing import Optional from unipoll_api.documents import ResourceID diff --git a/src/unipoll_api/utils/__init__.py b/src/unipoll_api/utils/__init__.py index e69de29..1c9bb5e 100644 --- a/src/unipoll_api/utils/__init__.py +++ b/src/unipoll_api/utils/__init__.py @@ -0,0 +1,7 @@ +from . import auth_strategy as AuthStrategy # noqa: F401 +from . import auth_transport as AuthTransport # noqa: F401 +from . import cli_args as ArgParser # noqa: F401 +from . import colored_dbg as Debug # noqa: F401 +from . import path_operations as PathOperations # noqa: F401 +from . import permissions as Permissions # noqa: F401 +from . import token_db as TokenDB # noqa: F401 diff --git a/src/unipoll_api/utils/token_db.py b/src/unipoll_api/utils/token_db.py index 0221610..b5899c7 100644 --- a/src/unipoll_api/utils/token_db.py +++ b/src/unipoll_api/utils/token_db.py @@ -3,7 +3,6 @@ Any, Dict, Generic, - # Literal, Optional, Type, TypeVar,