From 6fdd25e5056427eb599b8b81516164ee05cf8b8a Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sun, 8 Oct 2023 22:16:37 -0600 Subject: [PATCH 01/15] refactor: Moved group actions Moved actions to get list of groups and create new group to GroupActions --- src/unipoll_api/actions/group.py | 166 ++++++++++++++++++++++----- src/unipoll_api/actions/workspace.py | 82 +------------ src/unipoll_api/routes/workspace.py | 6 +- src/unipoll_api/schemas/group.py | 2 + 4 files changed, 146 insertions(+), 110 deletions(-) diff --git a/src/unipoll_api/actions/group.py b/src/unipoll_api/actions/group.py index e2f7b0f..9fb6725 100644 --- a/src/unipoll_api/actions/group.py +++ b/src/unipoll_api/actions/group.py @@ -1,27 +1,111 @@ -from beanie import DeleteRules +from beanie import DeleteRules, WriteRules from beanie.operators import In from unipoll_api import AccountManager -from unipoll_api.documents import Policy, ResourceID, Workspace, Group, Account +from unipoll_api.documents import Policy, ResourceID, Workspace, Group, Account, create_link 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 +from unipoll_api.utils import Permissions -# Get all groups (for superuser) -# async def get_all_groups() -> GroupSchemas.GroupList: -# group_list = [] -# search_result = await Group.find_all().to_list() +# Get list of groups +async def get_groups(workspace: Workspace | None = None, + account: Account | None = None, + name: str | None = None) -> GroupSchemas.GroupList: + account = account or AccountManager.active_user.get() -# # Create a group list for output schema using the search results -# for group in search_result: -# group_list.append(GroupSchemas.Group(**group.model_dump())) + # import time -# return GroupSchemas.GroupList(groups=group_list) + # Using Mongo operators + # t1 = time.time() + search_filter = {} + if name: + search_filter['name'] = name + if workspace: + search_filter['workspace._id'] = workspace.id + if account: + search_filter['members._id'] = account.id + search_result = await Group.find(search_filter, fetch_links=True).to_list() + # t2 = time.time() + # print("Mongo operator search time: ", t2 - t1) + + # Using Python operators + # t1 = time.time() + # search = Group.find_all(fetch_links=True) + # if name: + # search.find(Group.name == name) + # if workspace: + # search.find(Group.workspace.id == workspace.id, fetch_links=True) + # if account: + # search.find(Group.members.id == account.id, fetch_links=True) + # search_result = await search.to_list() + # t2 = time.time() + # print("Python operator search time: ", t2 - t1) + + groups = [] + for group in search_result: + try: + groups.append(await get_group(group=group)) + except Exception: + pass + + return GroupSchemas.GroupList(groups=groups) + + +# Create a new group with account as the owner +async def create_group(workspace: Workspace, + name: str, + description: str) -> GroupSchemas.GroupCreateOutput: + # await workspace.fetch_link(workspace.groups) + account = AccountManager.active_user.get() + + # Check if group name is unique + group: Group # For type hinting, until Link type is supported + for group in workspace.groups: # type: ignore + if group.name == name: + raise GroupExceptions.NonUniqueName(group) + + # Create a new group + new_group = Group(name=name, + description=description, + workspace=workspace) # type: ignore + + # Check if group was created + if not new_group: + raise GroupExceptions.ErrorWhileCreating(new_group) + + # Add the account to group member list + await new_group.add_member(account, Permissions.GROUP_ALL_PERMISSIONS) + + # Create a policy for the new group + permissions = Permissions.WORKSPACE_BASIC_PERMISSIONS # type: ignore + new_policy = Policy(policy_holder_type='group', + policy_holder=(await create_link(new_group)), + permissions=permissions, + parent_resource=workspace) # type: ignore + + # Add the group and the policy to the workspace + workspace.policies.append(new_policy) # type: ignore + workspace.groups.append(new_group) # type: ignore + await Workspace.save(workspace, link_rule=WriteRules.WRITE) + + # Return the new group + return GroupSchemas.GroupCreateOutput(**new_group.model_dump(include={'id', 'name', 'description'})) # Get group async def get_group(group: Group, include_members: bool = False, include_policies: bool = False) -> GroupSchemas.Group: + account = AccountManager.active_user.get() + + # Check if the user has a permission to get all the groups in the workspace + workspace_permissions = await Permissions.get_all_permissions(group.workspace, account) + group_permissions = await Permissions.get_all_permissions(group, account) + + if not (Permissions.check_permission(workspace_permissions, Permissions.WorkspacePermissions["get_groups"]) or + Permissions.check_permission(group_permissions, Permissions.GroupPermissions["get_group"])): + raise GroupExceptions.UserNotAuthorized( + account, group, f"to view group {group.id}") + members = (await get_group_members(group)).members if include_members else None policies = (await get_group_policies(group)).policies if include_policies else None workspace = WorkspaceSchemas.Workspace(**group.workspace.model_dump(exclude={"members", # type: ignore @@ -69,8 +153,10 @@ async def update_group(group: Group, async def delete_group(group: Group): # await group.fetch_link(Group.workspace) workspace: Workspace = group.workspace # type: ignore - workspace.groups = [g for g in workspace.groups if g.id != group.id] # type: ignore - workspace.policies = [p for p in workspace.policies if p.policy_holder.ref.id != group.id] # type: ignore + workspace.groups = [ + g for g in workspace.groups if g.id != group.id] # type: ignore + workspace.policies = [ + p for p in workspace.policies if p.policy_holder.ref.id != group.id] # type: ignore await Workspace.save(workspace, link_rule=DeleteRules.DELETE_LINKS) await Group.delete(group) @@ -85,10 +171,12 @@ async def get_group_members(group: Group) -> MemberSchemas.MemberList: account = AccountManager.active_user.get() permissions = await Permissions.get_all_permissions(group, account) - req_permissions = Permissions.GroupPermissions["get_group_members"] # type: ignore + # type: ignore + req_permissions = Permissions.GroupPermissions["get_group_members"] if Permissions.check_permission(permissions, req_permissions): for member in group.members: # type: ignore - member_data = member.model_dump(include={'id', 'first_name', 'last_name', 'email'}) + member_data = member.model_dump( + include={'id', 'first_name', 'last_name', 'email'}) member_scheme = MemberSchemas.Member(**member_data) member_list.append(member_scheme) # Return the list of members @@ -99,7 +187,8 @@ async def get_group_members(group: Group) -> MemberSchemas.MemberList: async def add_group_members(group: Group, member_data: MemberSchemas.AddMembers) -> MemberSchemas.MemberList: accounts = set(member_data.accounts) # Remove existing members from the accounts set - accounts = accounts.difference({member.id for member in group.members}) # type: ignore + accounts = accounts.difference( + {member.id for member in group.members}) # type: ignore # Find the accounts from the database account_list = await Account.find(In(Account.id, accounts)).to_list() # Add the accounts to the group member list with default permissions @@ -121,13 +210,15 @@ async def remove_group_member(group: Group, account_id: ResourceID | None): account = AccountManager.active_user.get() # Check if the account exists if not account: - raise ResourceExceptions.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) # Remove the account from the group if await group.remove_member(account): - member_list = [MemberSchemas.Member(**account.model_dump()) for account in group.members] # type: ignore + member_list = [MemberSchemas.Member( + **account.model_dump()) for account in group.members] # type: ignore return MemberSchemas.MemberList(members=member_list) raise GroupExceptions.ErrorWhileRemovingMember(group, account) @@ -138,26 +229,32 @@ async def get_group_policies(group: Group) -> PolicySchemas.PolicyList: policy: Policy account = AccountManager.active_user.get() permissions = await Permissions.get_all_permissions(group, account) - req_permissions = Permissions.GroupPermissions["get_group_policies"] # type: ignore + # type: ignore + req_permissions = Permissions.GroupPermissions["get_group_policies"] if Permissions.check_permission(permissions, req_permissions): for policy in group.policies: # type: ignore - permissions = Permissions.GroupPermissions(policy.permissions).name.split('|') # type: ignore + permissions = Permissions.GroupPermissions( + policy.permissions).name.split('|') # type: ignore # Get the policy_holder if policy.policy_holder_type == 'account': policy_holder = await Account.get(policy.policy_holder.ref.id) elif policy.policy_holder_type == 'group': policy_holder = await Group.get(policy.policy_holder.ref.id) else: - raise ResourceExceptions.InternalServerError("Invalid policy_holder_type") + raise ResourceExceptions.InternalServerError( + "Invalid policy_holder_type") if not policy_holder: # TODO: Replace with custom exception - raise ResourceExceptions.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.model_dump()) # type: ignore + policy_holder = MemberSchemas.Member( + **policy_holder.model_dump()) # type: ignore policy_list.append(PolicySchemas.PolicyShort(id=policy.id, policy_holder_type=policy.policy_holder_type, # Exclude unset fields(i.e. "description" for Account) - policy_holder=policy_holder.model_dump(exclude_unset=True), + policy_holder=policy_holder.model_dump( + exclude_unset=True), permissions=permissions)) return PolicySchemas.PolicyList(policies=policy_list) @@ -173,7 +270,8 @@ async def get_group_policy(group: Group, account_id: ResourceID | None): account = AccountManager.active_user.get() if not account: - raise ResourceExceptions.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]: @@ -207,7 +305,8 @@ async def set_group_policy(group: Group, account = AccountManager.active_user.get() # Make sure the account is loaded if not account: - raise ResourceExceptions.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 @@ -223,21 +322,26 @@ async def set_group_policy(group: Group, new_permission_value = 0 for i in input_data.permissions: try: - new_permission_value += Permissions.GroupPermissions[i].value # type: ignore + # type: ignore + new_permission_value += Permissions.GroupPermissions[i].value except KeyError: raise ResourceExceptions.InvalidPermission(i) # Update the policy - policy.permissions = Permissions.GroupPermissions(new_permission_value) # type: ignore + policy.permissions = Permissions.GroupPermissions( + new_permission_value) # type: ignore await Policy.save(policy) # Get Account or Group from policy_holder link # HACK: Have to do it manualy, as Beanie cannot fetch policy_holder link of mixed types (Account | Group) if policy.policy_holder_type == "account": # type: ignore - policy_holder = await Account.get(policy.policy_holder.ref.id) # type: ignore + # type: ignore + policy_holder = await Account.get(policy.policy_holder.ref.id) elif policy.policy_holder_type == "group": # type: ignore - policy_holder = await Group.get(policy.policy_holder.ref.id) # type: ignore + # type: ignore + policy_holder = await Group.get(policy.policy_holder.ref.id) # Return the updated policy return PolicySchemas.PolicyOutput( - permissions=Permissions.GroupPermissions(policy.permissions).name.split('|'), # type: ignore + permissions=Permissions.GroupPermissions( + policy.permissions).name.split('|'), # type: ignore policy_holder=MemberSchemas.Member(**policy_holder.model_dump())) # type: ignore diff --git a/src/unipoll_api/actions/workspace.py b/src/unipoll_api/actions/workspace.py index 0c3c462..7d0ff6d 100644 --- a/src/unipoll_api/actions/workspace.py +++ b/src/unipoll_api/actions/workspace.py @@ -3,16 +3,17 @@ from beanie import WriteRules, DeleteRules from beanie.operators import In from unipoll_api import AccountManager -from unipoll_api.documents import Group, ResourceID, Workspace, Account, Policy, Poll, create_link +from unipoll_api.actions import GroupActions +from unipoll_api.documents import Group, ResourceID, Workspace, Account, Policy, Poll from unipoll_api.actions import PolicyActions, PollActions 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, +from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, MemberSchemas, PollSchemas +from unipoll_api.exceptions import (WorkspaceExceptions, AccountExceptions, ResourceExceptions, PolicyExceptions, PollExceptions) # Get a list of workspaces where the account is a owner/member -async def get_workspaces() -> WorkspaceSchemas.WorkspaceList: +async def get_workspaces(account: Account | None = None) -> WorkspaceSchemas.WorkspaceList: account = AccountManager.active_user.get() workspace_list = [] @@ -40,16 +41,6 @@ async def create_workspace(input_data: WorkspaceSchemas.WorkspaceCreateInput) -> if not new_workspace: raise WorkspaceExceptions.ErrorWhileCreating(input_data.name) - # Create a policy for the new member - # The member(creator) has full permissions on the workspace - # new_policy = Policy(policy_holder_type='account', - # policy_holder=(await create_link(account)), - # permissions=Permissions.WORKSPACE_ALL_PERMISSIONS, - # parent_resource=new_workspace) # type: ignore - - # Add the current user and the policy to workspace member list - # new_workspace.members.append(account) # type: ignore - # new_workspace.policies.append(new_policy) # type: ignore await new_workspace.add_member(account=account, permissions=Permissions.WORKSPACE_ALL_PERMISSIONS, save=False) await Workspace.save(new_workspace, link_rule=WriteRules.WRITE) @@ -63,7 +54,7 @@ async def get_workspace(workspace: Workspace, include_policies: bool = False, include_members: bool = False, include_polls: bool = False) -> WorkspaceSchemas.Workspace: - groups = (await get_groups(workspace)).groups if include_groups else None + groups = (await GroupActions.get_groups(workspace)).groups if include_groups else None members = (await get_workspace_members(workspace)).members if include_members else None policies = (await get_workspace_policies(workspace)).policies if include_policies else None polls = (await get_polls(workspace)).polls if include_polls else None @@ -164,67 +155,6 @@ async def remove_workspace_member(workspace: Workspace, account_id: ResourceID): raise WorkspaceExceptions.ErrorWhileRemovingMember(workspace, account) -# Get a list of groups where the account is a member -async def get_groups(workspace: Workspace) -> GroupSchemas.GroupList: - account = AccountManager.active_user.get() - permissions = await Permissions.get_all_permissions(workspace, account) - # Check if the user has permission to get all groups - req_permissions = Permissions.WorkspacePermissions["get_groups"] # type: ignore - if Permissions.check_permission(permissions, req_permissions): - groups = [GroupSchemas.GroupShort(**group.model_dump()) for group in workspace.groups] # type: ignore - # Otherwise, return only the groups where the user has permission to get the group - else: - groups = [] - for group in workspace.groups: - user_permissions = await Permissions.get_all_permissions(group, account) - required_permission = Permissions.GroupPermissions['get_group'] - if Permissions.check_permission(Permissions.GroupPermissions(user_permissions), # type: ignore - required_permission): - groups.append(GroupSchemas.GroupShort(**group.model_dump())) # type: ignore - # Return the list of groups - return GroupSchemas.GroupList(groups=groups) - - -# Create a new group with account as the owner -async def create_group(workspace: Workspace, - input_data: GroupSchemas.GroupCreateInput) -> GroupSchemas.GroupCreateOutput: - # await workspace.fetch_link(workspace.groups) - account = AccountManager.active_user.get() - - # Check if group name is unique - group: Group # For type hinting, until Link type is supported - for group in workspace.groups: # type: ignore - if group.name == input_data.name: - raise GroupExceptions.NonUniqueName(group) - - # Create a new group - new_group = Group(name=input_data.name, - description=input_data.description, - workspace=workspace) # type: ignore - - # Check if group was created - if not new_group: - raise GroupExceptions.ErrorWhileCreating(new_group) - - # Add the account to group member list - await new_group.add_member(account, Permissions.GROUP_ALL_PERMISSIONS) - - # Create a policy for the new group - permissions = Permissions.WORKSPACE_BASIC_PERMISSIONS # type: ignore - new_policy = Policy(policy_holder_type='group', - policy_holder=(await create_link(new_group)), - permissions=permissions, - parent_resource=workspace) # type: ignore - - # Add the group and the policy to the workspace - workspace.policies.append(new_policy) # type: ignore - workspace.groups.append(new_group) # type: ignore - await Workspace.save(workspace, link_rule=WriteRules.WRITE) - - # Return the new group - return GroupSchemas.GroupCreateOutput(**new_group.model_dump(include={'id', 'name', 'description'})) - - # Get all policies of a workspace async def get_workspace_policies(workspace: Workspace) -> PolicySchemas.PolicyList: policy_list = await PolicyActions.get_policies(resource=workspace) diff --git a/src/unipoll_api/routes/workspace.py b/src/unipoll_api/routes/workspace.py index 92159ca..7af05ad 100644 --- a/src/unipoll_api/routes/workspace.py +++ b/src/unipoll_api/routes/workspace.py @@ -2,7 +2,7 @@ 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 WorkspaceActions, PermissionsActions +from unipoll_api.actions import WorkspaceActions, PermissionsActions, GroupActions from unipoll_api.exceptions.resource import APIException from unipoll_api.documents import Workspace, ResourceID from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, GroupSchemas, MemberSchemas, PollSchemas @@ -159,7 +159,7 @@ async def delete_workspace(workspace: Workspace = Depends(Dependencies.get_works response_model=GroupSchemas.GroupList) async def get_groups(workspace: Workspace = Depends(Dependencies.get_workspace_model)): try: - return await WorkspaceActions.get_groups(workspace) + return await GroupActions.get_groups(workspace) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -172,7 +172,7 @@ async def get_groups(workspace: Workspace = Depends(Dependencies.get_workspace_m async def create_group(workspace: Workspace = Depends(Dependencies.get_workspace_model), input_data: GroupSchemas.GroupCreateInput = Body(...)): try: - return await WorkspaceActions.create_group(workspace, input_data) + return await GroupActions.create_group(workspace, input_data.name, input_data.description) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) diff --git a/src/unipoll_api/schemas/group.py b/src/unipoll_api/schemas/group.py index 95c6a01..ffed75d 100644 --- a/src/unipoll_api/schemas/group.py +++ b/src/unipoll_api/schemas/group.py @@ -28,10 +28,12 @@ class GroupList(BaseModel): # Schema for the request to create a new group class GroupCreateInput(BaseModel): name: str = Field(default="", min_length=3, max_length=50) + workspace: ResourceID = Field(default=None, title="Workspace ID") description: str = Field(default="", title="Description", max_length=300) model_config = ConfigDict(json_schema_extra={ "example": { "name": "Group 01", + workspace: "60b9d1c8e1f1d5f5f5b4f8e1", "description": "My first Group", } }) From ff9aa0256aff8f453035e31a13759e97dd3b0fa8 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sun, 8 Oct 2023 22:19:22 -0600 Subject: [PATCH 02/15] feat: Added new routes to get and create groups Added new route to get list of groups, by default returns all groups which a user has permission to view, the endpoint also accepts query parameters to filter list of groups by name, workspace, or account --- src/unipoll_api/routes/group.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/unipoll_api/routes/group.py b/src/unipoll_api/routes/group.py index 741f7ac..1078803 100644 --- a/src/unipoll_api/routes/group.py +++ b/src/unipoll_api/routes/group.py @@ -13,10 +13,31 @@ router: APIRouter = APIRouter(dependencies=[Depends(Dependencies.check_group_permission)]) -# Get all groups -# @router.get("/", response_description="Get all groups") -# async def get_all_groups() -> GroupSchemas.GroupList: -# return await GroupActions.get_all_groups() +# Get groups +@open_router.get("/", response_description="List of groups") +async def get_all_groups(workspace: Annotated[ResourceID | None, Query()] = None, + account: Annotated[ResourceID | None, Query()] = None, + name: Annotated[str | None, Query()] = None + ) -> GroupSchemas.GroupList: + args = {} + args['workspace'] = await Dependencies.get_workspace_model(workspace) if workspace else None + args['account'] = await Dependencies.get_account(account) if account else None + args['name'] = name + + return await GroupActions.get_groups(**args) + + +# Create a new group +@open_router.post("/", + status_code=201, + response_description="Created Group", + response_model=GroupSchemas.GroupCreateOutput) +async def create_group(input_data: GroupSchemas.GroupCreateInput = Body(...)): + try: + workspace = await Dependencies.get_workspace_model(input_data.workspace) + return await GroupActions.create_group(workspace, name=input_data.name, description=input_data.description) + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) query_params = list[Literal["policies", "members", "all"]] @@ -34,7 +55,7 @@ async def get_group(group: Group = Depends(Dependencies.get_group_model), params = {} if include: if "all" in include: - params = {"include_members": True, "include_polls": True} + params = {"include_members": True, "include_policies": True} else: if "members" in include: params["include_members"] = True From 3bd5363cd5b05db28e15dcdfc649692aa117a98c Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Mon, 9 Oct 2023 22:20:28 -0600 Subject: [PATCH 03/15] refactor: Changed Member Actions Moved Members related actions to a separate module. The old routes use new actions to view, add, remove members from Group/Workspace --- src/unipoll_api/actions/__init__.py | 1 + src/unipoll_api/actions/group.py | 121 ++++++++++++++------------- src/unipoll_api/actions/members.py | 91 ++++++++++++++++++++ src/unipoll_api/actions/workspace.py | 66 ++------------- src/unipoll_api/documents.py | 93 ++++++++++++++------ src/unipoll_api/routes/group.py | 13 +-- src/unipoll_api/routes/workspace.py | 13 +-- src/unipoll_api/schemas/member.py | 27 +++++- 8 files changed, 266 insertions(+), 159 deletions(-) create mode 100644 src/unipoll_api/actions/members.py diff --git a/src/unipoll_api/actions/__init__.py b/src/unipoll_api/actions/__init__.py index c350a2e..8e49fda 100644 --- a/src/unipoll_api/actions/__init__.py +++ b/src/unipoll_api/actions/__init__.py @@ -5,3 +5,4 @@ from . import authentication as AuthActions # noqa: F401 from . import workspace as WorkspaceActions # noqa: F401 from . import permissions as PermissionsActions # noqa: F401 +from . import members as MembersActions # noqa: F401 diff --git a/src/unipoll_api/actions/group.py b/src/unipoll_api/actions/group.py index 9fb6725..5b36c47 100644 --- a/src/unipoll_api/actions/group.py +++ b/src/unipoll_api/actions/group.py @@ -2,6 +2,7 @@ from beanie.operators import In from unipoll_api import AccountManager from unipoll_api.documents import Policy, ResourceID, Workspace, Group, Account, create_link +from unipoll_api import actions from unipoll_api.schemas import AccountSchemas, GroupSchemas, MemberSchemas, PolicySchemas, WorkspaceSchemas from unipoll_api.exceptions import (AccountExceptions, GroupExceptions, PolicyExceptions, ResourceExceptions, WorkspaceExceptions) @@ -45,7 +46,7 @@ async def get_groups(workspace: Workspace | None = None, groups = [] for group in search_result: try: - groups.append(await get_group(group=group)) + groups.append((await get_group(group=group)).model_dump(exclude_none=True)) except Exception: pass @@ -106,7 +107,7 @@ async def get_group(group: Group, include_members: bool = False, include_policie raise GroupExceptions.UserNotAuthorized( account, group, f"to view group {group.id}") - members = (await get_group_members(group)).members if include_members else None + members = (await actions.MembersActions.get_members(group)).members if include_members else None policies = (await get_group_policies(group)).policies if include_policies else None workspace = WorkspaceSchemas.Workspace(**group.workspace.model_dump(exclude={"members", # type: ignore "policies", @@ -164,63 +165,67 @@ async def delete_group(group: Group): return GroupExceptions.ErrorWhileDeleting(group.id) -# Get list of members of a group -async def get_group_members(group: Group) -> MemberSchemas.MemberList: - member_list = [] - member: Account +# # Get list of members of a group +# async def get_group_members(group: Group) -> MemberSchemas.MemberList: +# member_list = [] +# member: Account + +# account = AccountManager.active_user.get() +# permissions = await Permissions.get_all_permissions(group, account) +# # type: ignore +# req_permissions = Permissions.GroupPermissions["get_group_members"] +# if Permissions.check_permission(permissions, req_permissions): +# for member in group.members: # type: ignore +# member_data = member.model_dump( +# include={'id', 'first_name', 'last_name', 'email'}) +# member_scheme = MemberSchemas.Member(**member_data) +# member_list.append(member_scheme) +# # Return the list of members +# return MemberSchemas.MemberList(members=member_list) + + +# async def add_member(group: Group, account: "Account", permissions, save: bool = True) -> "Account": +# if account.id not in [member.id for member in self.workspace.members]: # type: ignore +# raise Exceptions.WorkspaceExceptions.UserNotMember(self.workspace, account) # type: ignore +# # Add the account to the group +# self.members.append(account) # type: ignore +# # Create a policy for the new member +# new_policy = Policy(policy_holder_type='account', +# policy_holder=(await create_link(account)), +# permissions=permissions, +# parent_resource=self) # type: ignore + +# # Add the policy to the group +# self.policies.append(new_policy) # type: ignore +# if save: +# await self.save(link_rule=WriteRules.WRITE) # type: ignore +# return account + + +# async def remove_member(self, account, save: bool = True) -> bool: +# # Remove the account from the group +# for i, member in enumerate(self.members): +# if account.id == member.id: # type: ignore +# self.members.remove(member) +# Debug.info(f"Removed member {member.id} from {self.resource_type} {self.id}") # type: ignore +# break + +# # Remove the policy from the group +# for policy in self.policies: +# pc = policy.policy_holder # type: ignore +# if pc.ref.id == account.id: +# self.policies.remove(policy) +# await Policy.delete(policy) +# Debug.info(f"Removed policy: {pc.ref.id} from {self.resource_type} {self.id}") +# break + +# if save: +# await self.save(link_rule=WriteRules.WRITE) # type: ignore +# return True + + + - account = AccountManager.active_user.get() - permissions = await Permissions.get_all_permissions(group, account) - # type: ignore - req_permissions = Permissions.GroupPermissions["get_group_members"] - if Permissions.check_permission(permissions, req_permissions): - for member in group.members: # type: ignore - member_data = member.model_dump( - include={'id', 'first_name', 'last_name', 'email'}) - member_scheme = MemberSchemas.Member(**member_data) - member_list.append(member_scheme) - # Return the list of members - return MemberSchemas.MemberList(members=member_list) - - -# Add groups/members to group -async def add_group_members(group: Group, member_data: MemberSchemas.AddMembers) -> MemberSchemas.MemberList: - accounts = set(member_data.accounts) - # Remove existing members from the accounts set - accounts = accounts.difference( - {member.id for member in group.members}) # type: ignore - # Find the accounts from the database - account_list = await Account.find(In(Account.id, accounts)).to_list() - # Add the accounts to the group member list with default permissions - for account in account_list: - await group.add_member(account, Permissions.GROUP_BASIC_PERMISSIONS) - await Group.save(group) - # Return the list of members added to the group - return MemberSchemas.MemberList(members=[MemberSchemas.Member(**account.model_dump()) for account in account_list]) - - -# Remove a member from a workspace -async def remove_group_member(group: Group, account_id: ResourceID | None): - # Check if account_id is specified in request, if account_id is not specified, use the current user - if account_id: - account = await Account.get(account_id) # type: ignore - if not account: - raise AccountExceptions.AccountNotFound(account_id) - else: - account = AccountManager.active_user.get() - # Check if the account exists - if not account: - 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) - # Remove the account from the group - if await group.remove_member(account): - member_list = [MemberSchemas.Member( - **account.model_dump()) for account in group.members] # type: ignore - return MemberSchemas.MemberList(members=member_list) - raise GroupExceptions.ErrorWhileRemovingMember(group, account) # Get all policies of a group diff --git a/src/unipoll_api/actions/members.py b/src/unipoll_api/actions/members.py new file mode 100644 index 0000000..0d556ca --- /dev/null +++ b/src/unipoll_api/actions/members.py @@ -0,0 +1,91 @@ +from beanie import WriteRules +from beanie.operators import In +from unipoll_api.documents import Account, Group, ResourceID, Workspace +from unipoll_api.utils import Permissions +from unipoll_api.schemas import MemberSchemas +from unipoll_api import AccountManager +from unipoll_api.exceptions import ResourceExceptions, AccountExceptions + + +async def get_members(resource: Workspace | Group) -> MemberSchemas.MemberList: + account = AccountManager.active_user.get() + permissions = await Permissions.get_all_permissions(resource, account) + + if resource.resource_type == "workspace": + req_permissions = Permissions.WorkspacePermissions["get_workspace_members"] + elif resource.resource_type == "group": + req_permissions = Permissions.GroupPermissions["get_group_members"] + else: + raise ResourceExceptions.InternalServerError("Invalid resource type") + + if not Permissions.check_permission(permissions, req_permissions): + ResourceExceptions.UserNotAuthorized(account, resource.resource_type, "to view members") + + def build_member_scheme(member: Account) -> MemberSchemas.Member: + member_data = member.model_dump(include={'id', 'first_name', 'last_name', 'email'}) + member_scheme = MemberSchemas.Member(**member_data) + return member_scheme + + member_list = [build_member_scheme(member) for member in resource.members] # type: ignore + # Return the list of members + return MemberSchemas.MemberList(members=member_list) + + +# Add groups/members to group +async def add_members(resource: Workspace | Group, + account_id_list: list[ResourceID]) -> MemberSchemas.MemberList: + # Check if the user has permission to add members + account = AccountManager.active_user.get() + permissions = await Permissions.get_all_permissions(resource, account) + if resource.resource_type == "workspace": + req_permissions = Permissions.WorkspacePermissions["add_workspace_members"] + default_permissions = Permissions.WORKSPACE_BASIC_PERMISSIONS + elif resource.resource_type == "group": + req_permissions = Permissions.GroupPermissions["add_group_members"] + default_permissions = Permissions.GROUP_BASIC_PERMISSIONS + else: + raise ResourceExceptions.InternalServerError("Invalid resource type") + + if not Permissions.check_permission(permissions, req_permissions): + ResourceExceptions.UserNotAuthorized(account, resource.resource_type, "to add members") + + # Remove duplicates from the list of accounts + accounts = set(account_id_list) + # Remove existing members from the accounts set + accounts = accounts.difference({member.id for member in resource.members}) # type: ignore + # Find the accounts from the database + account_list = await Account.find(In(Account.id, accounts)).to_list() + # Add the accounts to the group member list with basic permissions + + for account in account_list: + await resource.add_member(account, default_permissions, save=False) + await resource.save(link_rule=WriteRules.WRITE) # type: ignore + + # Return the list of members added to the group + return MemberSchemas.MemberList(members=[MemberSchemas.Member(**account.model_dump()) for account in account_list]) + + +# Remove a member from a workspace +async def remove_member(resource: Workspace | Group, account: Account): + # Check if the user has permission to add members + account = AccountManager.active_user.get() + permissions = await Permissions.get_all_permissions(resource, account) + if resource.resource_type == "workspace": + req_permissions = Permissions.WorkspacePermissions["remove_workspace_members"] + elif resource.resource_type == "group": + req_permissions = Permissions.GroupPermissions["remove_group_members"] + else: + raise ResourceExceptions.InternalServerError("Invalid resource type") + if not Permissions.check_permission(permissions, req_permissions): + ResourceExceptions.UserNotAuthorized(account, resource.resource_type, "to add members") + + # Check if the account is a member of the workspace + if account.id not in [ResourceID(member.id) for member in resource.members]: # type: ignore + raise ResourceExceptions.UserNotMember(resource, account) + + # Remove the account from the workspace/group + if await resource.remove_member(account): + # Return the list of members added to the group + member_list = [MemberSchemas.Member(**account.model_dump()) for account in resource.members] # type: ignore + return MemberSchemas.MemberList(members=member_list) + raise ResourceExceptions.ErrorWhileRemovingMember(resource, account) diff --git a/src/unipoll_api/actions/workspace.py b/src/unipoll_api/actions/workspace.py index 7d0ff6d..9cc10c3 100644 --- a/src/unipoll_api/actions/workspace.py +++ b/src/unipoll_api/actions/workspace.py @@ -1,11 +1,9 @@ # from typing import Optional # from pydantic import EmailStr from beanie import WriteRules, DeleteRules -from beanie.operators import In from unipoll_api import AccountManager -from unipoll_api.actions import GroupActions +from unipoll_api import actions from unipoll_api.documents import Group, ResourceID, Workspace, Account, Policy, Poll -from unipoll_api.actions import PolicyActions, PollActions from unipoll_api.utils import Permissions from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, MemberSchemas, PollSchemas from unipoll_api.exceptions import (WorkspaceExceptions, AccountExceptions, ResourceExceptions, @@ -54,8 +52,8 @@ async def get_workspace(workspace: Workspace, include_policies: bool = False, include_members: bool = False, include_polls: bool = False) -> WorkspaceSchemas.Workspace: - groups = (await GroupActions.get_groups(workspace)).groups if include_groups else None - members = (await get_workspace_members(workspace)).members if include_members else None + groups = (await actions.GroupActions.get_groups(workspace)).groups if include_groups else None + members = (await actions.MembersActions.get_members(workspace)).members if include_members else None policies = (await get_workspace_policies(workspace)).policies if include_policies else None polls = (await get_polls(workspace)).polls if include_polls else None # Return the workspace with the fetched resources @@ -100,64 +98,12 @@ async def delete_workspace(workspace: Workspace): await Group.find(Group.workspace.id == workspace).delete() # type: ignore -# List all members of a workspace -async def get_workspace_members(workspace: Workspace) -> MemberSchemas.MemberList: - member_list = [] - member: Account - account: Account = AccountManager.active_user.get() - - permissions = await Permissions.get_all_permissions(workspace, account) - req_permissions = Permissions.WorkspacePermissions["get_workspace_members"] # type: ignore - if Permissions.check_permission(permissions, req_permissions): - for member in workspace.members: # type: ignore - member_data = member.model_dump(include={'id', 'first_name', 'last_name', 'email'}) - member_scheme = MemberSchemas.Member(**member_data) - member_list.append(member_scheme) - # Return the list of members - return MemberSchemas.MemberList(members=member_list) - - -# Add groups/members to group -async def add_workspace_members(workspace: Workspace, - member_data: MemberSchemas.AddMembers) -> MemberSchemas.MemberList: - accounts = set(member_data.accounts) - # Remove existing members from the accounts set - accounts = accounts.difference({member.id for member in workspace.members}) # type: ignore - # Find the accounts from the database - account_list = await Account.find(In(Account.id, accounts)).to_list() - # Add the accounts to the group member list with basic permissions - for account in account_list: - await workspace.add_member(account, Permissions.WORKSPACE_BASIC_PERMISSIONS, save=False) - await Workspace.save(workspace, link_rule=WriteRules.WRITE) - # Return the list of members added to the group - return MemberSchemas.MemberList(members=[MemberSchemas.Member(**account.model_dump()) for account in account_list]) - - -# Remove a member from a workspace -async def remove_workspace_member(workspace: Workspace, account_id: ResourceID): - # Check if account_id is specified in request, if account_id is not specified, use the current user - if account_id: - account = await Account.get(account_id) # type: ignore - else: - account = AccountManager.active_user.get() - # Check if the account exists - if not account: - raise AccountExceptions.AccountNotFound(account_id) - # Check if the account is a member of the workspace - if account.id not in [ResourceID(member.id) for member in workspace.members]: # type: ignore - raise WorkspaceExceptions.UserNotMember(workspace, account) - # Remove the account from the workspace - if await workspace.remove_member(account): - # Return the list of members added to the group - member_list = [MemberSchemas.Member(**account.model_dump()) for account in workspace.members] # type: ignore - return MemberSchemas.MemberList(members=member_list) - raise WorkspaceExceptions.ErrorWhileRemovingMember(workspace, account) # Get all policies of a workspace async def get_workspace_policies(workspace: Workspace) -> PolicySchemas.PolicyList: - policy_list = await PolicyActions.get_policies(resource=workspace) + policy_list = await actions.PolicyActions.get_policies(resource=workspace) return PolicySchemas.PolicyList(policies=policy_list.policies) @@ -167,7 +113,7 @@ 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 AccountManager.active_user.get() # type: ignore - policy_list = await PolicyActions.get_policies(resource=workspace, policy_holder=account) + policy_list = await actions.PolicyActions.get_policies(resource=workspace, policy_holder=account) user_policy = policy_list.policies[0] return PolicySchemas.PolicyOutput( @@ -240,7 +186,7 @@ async def set_workspace_policy(workspace: Workspace, # Get a list of polls in a workspace async def get_polls(workspace: Workspace) -> PollSchemas.PollList: - return await PollActions.get_polls(workspace) + return await actions.PollActions.get_polls(workspace) # Create a new poll in a workspace diff --git a/src/unipoll_api/documents.py b/src/unipoll_api/documents.py index d489959..a64245b 100644 --- a/src/unipoll_api/documents.py +++ b/src/unipoll_api/documents.py @@ -41,43 +41,23 @@ class Resource(Document): def create_group(self) -> None: Debug.info(f'New {self.resource_type} "{self.id}" has been created') - async def add_member(self, account: "Account", permissions, save: bool = True) -> "Account": - # Add the account to the group - self.members.append(account) # type: ignore - # Create a policy for the new member + async def add_policy(self, member: "Group | Account", permissions, save: bool = True) -> None: new_policy = Policy(policy_holder_type='account', - policy_holder=(await create_link(account)), + policy_holder=(await create_link(member)), permissions=permissions, parent_resource=self) # type: ignore # Add the policy to the group self.policies.append(new_policy) # type: ignore if save: - await Resource.save(self, link_rule=WriteRules.WRITE) - return account - - async def remove_member(self, account, save: bool = True) -> bool: - # Remove the account from the group - # await self.fetch_link("members") - for i, member in enumerate(self.members): - if account.id == member.id: # type: ignore - self.members.remove(member) - Debug.info(f"Removed member {member.id} from {self.resource_type} {self.id}") - break + await self.save(link_rule=WriteRules.WRITE) # type: ignore - # Remove the policy from the group - # await self.fetch_link("policies") + async def remove_policy(self, member: "Group | Account", save: bool = True) -> None: for policy in self.policies: - # pc = await policy.policy_holder.fetch() # type: ignore - pc = policy.policy_holder # type: ignore - if pc.ref.id == account.id: + if policy.policy_holder.id == member.id: # type: ignore self.policies.remove(policy) - await Policy.delete(policy) - Debug.info(f"Removed policy: {pc.ref.id} from {self.resource_type} {self.id}") - break - - await self.save(link_rule=WriteRules.WRITE) # type: ignore - return True + if save: + await self.save(link_rule=WriteRules.WRITE) # type: ignore class Account(BeanieBaseUser, Document): # type: ignore @@ -100,6 +80,37 @@ class Workspace(Resource): groups: list[Link["Group"]] = [] polls: list[Link["Poll"]] = [] + async def add_member(self, account: "Account", permissions, save: bool = True) -> "Account": + # Add the account to the group + self.members.append(account) # type: ignore + # Create a policy for the new member + await self.add_policy(account, permissions, save=False) # type: ignore + if save: + await self.save(link_rule=WriteRules.WRITE) # type: ignore + return account + + async def remove_member(self, account, save: bool = True) -> bool: + # Remove the account from the group + for i, member in enumerate(self.members): + if account.id == member.id: # type: ignore + self.members.remove(member) + Debug.info(f"Removed member {member.id} from {self.resource_type} {self.id}") # type: ignore + break + + # Remove the policy from the workspace + await self.remove_policy(account, save=False) # type: ignore + + # Remove the member from all groups in the workspace + group: Group + for group in self.groups: # type: ignore + await group.remove_member(account, save=False) + await group.remove_policy(account, save=False) + await Group.save(group, link_rule=WriteRules.WRITE) + + if save: + await self.save(link_rule=WriteRules.WRITE) # type: ignore + return True + class Group(Resource): resource_type: Literal["group"] = "group" @@ -107,6 +118,34 @@ class Group(Resource): members: list[Link["Account"]] = [] groups: list[Link["Group"]] = [] + async def add_member(self, account: "Account", permissions, save: bool = True) -> "Account": + if account not in self.workspace.members: # type: ignore + from unipoll_api.exceptions import WorkspaceExceptions + raise WorkspaceExceptions.UserNotMember(self.workspace, account) # type: ignore + + # Add the account to the group + self.members.append(account) # type: ignore + # Create a policy for the new member + await self.add_policy(account, permissions, save=False) # type: ignore + if save: + await self.save(link_rule=WriteRules.WRITE) # type: ignore + return account + + async def remove_member(self, account, save: bool = True) -> bool: + # Remove the account from the group + for i, member in enumerate(self.members): + if account.id == member.id: # type: ignore + self.members.remove(member) + Debug.info(f"Removed member {member.id} from {self.resource_type} {self.id}") # type: ignore + break + + # Remove the policy from the group + await self.remove_policy(account, save=False) # type: ignore + + if save: + await self.save(link_rule=WriteRules.WRITE) # type: ignore + return True + class Policy(Document): id: ResourceID = Field(default_factory=ResourceID, alias="_id") diff --git a/src/unipoll_api/routes/group.py b/src/unipoll_api/routes/group.py index 1078803..4b53549 100644 --- a/src/unipoll_api/routes/group.py +++ b/src/unipoll_api/routes/group.py @@ -2,10 +2,10 @@ 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 GroupActions, PermissionsActions +from unipoll_api.actions import GroupActions, PermissionsActions, MembersActions from unipoll_api.exceptions.resource import APIException from unipoll_api.schemas import GroupSchemas, PolicySchemas, MemberSchemas -from unipoll_api.documents import Group, ResourceID +from unipoll_api.documents import Account, Group, ResourceID # APIRouter creates path operations for user module @@ -97,7 +97,7 @@ async def delete_group(group: Group = Depends(Dependencies.get_group_model)): response_model_exclude_unset=True) async def get_group_members(group: Group = Depends(Dependencies.get_group_model)): try: - return await GroupActions.get_group_members(group) + return await MembersActions.get_members(group) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -109,7 +109,7 @@ async def get_group_members(group: Group = Depends(Dependencies.get_group_model) async def add_group_members(member_data: MemberSchemas.AddMembers, group: Group = Depends(Dependencies.get_group_model)): try: - return await GroupActions.add_group_members(group, member_data) + return await MembersActions.add_members(group, member_data.accounts) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -119,9 +119,10 @@ async def add_group_members(member_data: MemberSchemas.AddMembers, response_description="Updated list removed members", response_model_exclude_unset=True) async def remove_group_member(group: Group = Depends(Dependencies.get_group_model), - account_id: ResourceID = Path(..., description="Account ID of the member to remove")): + account: Account = Depends(Dependencies.get_account)): + # account_id: ResourceID = Path(..., description="Account ID of the member to remove")): try: - return await GroupActions.remove_group_member(group, account_id) + return await MembersActions.remove_member(group, account) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) diff --git a/src/unipoll_api/routes/workspace.py b/src/unipoll_api/routes/workspace.py index 7af05ad..6c97f59 100644 --- a/src/unipoll_api/routes/workspace.py +++ b/src/unipoll_api/routes/workspace.py @@ -2,9 +2,9 @@ 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 WorkspaceActions, PermissionsActions, GroupActions +from unipoll_api.actions import WorkspaceActions, PermissionsActions, GroupActions, MembersActions from unipoll_api.exceptions.resource import APIException -from unipoll_api.documents import Workspace, ResourceID +from unipoll_api.documents import Account, Workspace, ResourceID from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, GroupSchemas, MemberSchemas, PollSchemas # APIRouter creates path operations for user module @@ -184,7 +184,7 @@ async def create_group(workspace: Workspace = Depends(Dependencies.get_workspace response_model_exclude_unset=True) async def get_workspace_members(workspace: Workspace = Depends(Dependencies.get_workspace_model)): try: - return await WorkspaceActions.get_workspace_members(workspace) + return await MembersActions.get_members(workspace) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -196,7 +196,7 @@ async def get_workspace_members(workspace: Workspace = Depends(Dependencies.get_ async def add_workspace_members(workspace: Workspace = Depends(Dependencies.get_workspace_model), member_data: MemberSchemas.AddMembers = Body(...)): try: - return await WorkspaceActions.add_workspace_members(workspace, member_data) + return await MembersActions.add_members(workspace, member_data.accounts) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -206,9 +206,10 @@ async def add_workspace_members(workspace: Workspace = Depends(Dependencies.get_ response_description="Updated list removed members", response_model_exclude_unset=True) async def remove_workspace_member(workspace: Workspace = Depends(Dependencies.get_workspace_model), - account_id: ResourceID = Path(..., description="Account ID of the member to remove")): + account: Account = Depends(Dependencies.get_account)): + # account_id: ResourceID = Path(..., description="Account ID of the member to remove")): try: - return await WorkspaceActions.remove_workspace_member(workspace, account_id) + return await MembersActions.remove_member(workspace, account) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) diff --git a/src/unipoll_api/schemas/member.py b/src/unipoll_api/schemas/member.py index b4b8c56..69b782f 100644 --- a/src/unipoll_api/schemas/member.py +++ b/src/unipoll_api/schemas/member.py @@ -1,5 +1,5 @@ from typing import Optional -from pydantic import ConfigDict, BaseModel, EmailStr, Field +from pydantic import ConfigDict, BaseModel, EmailStr, Field, root_validator from unipoll_api.documents import ResourceID @@ -21,7 +21,6 @@ class Member(BaseModel): }) -# Schema for the request to add a member to a workspace class AddMembers(BaseModel): accounts: list[ResourceID] = Field(title="Accounts") model_config = ConfigDict(json_schema_extra={ @@ -35,6 +34,30 @@ class AddMembers(BaseModel): }) +# Schema for the request to add a member to a workspace +class AddMembersRequest(BaseModel): + accounts: list[ResourceID] = Field(title="Accounts") + workspace: Optional[ResourceID] = Field(title="Workspace") + group: Optional[ResourceID] = Field(title="Group") + + # Validate that either workspace or group is specified + @root_validator(pre=True) + def validate_resource(cls, values): + if sum([bool(v) for v in values.values()]) != 2: + raise ValueError('Either Workspace or Groups must be specified.') + return values + + model_config = ConfigDict(validate_assignment=True, json_schema_extra={ + "example": { + "accounts": [ + "1a2b3c4d5e6f7g8h9i0j", + "2a3b4c5d6e7f8g9h0i1j", + "3a4b5c6d7e8f9g0h1i2j" + ] + } + },) + + # Schema for the response with a list of members and their info class MemberList(BaseModel): members: list[Member] From 5129fbe7d6b9b68f8aeb515e2ca35a4726faeeb1 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Tue, 10 Oct 2023 23:26:14 -0600 Subject: [PATCH 04/15] feat: Moved policy related actions to a separate file --- src/unipoll_api/actions/group.py | 197 +-------------------------- src/unipoll_api/actions/policy.py | 89 ++++++++++-- src/unipoll_api/actions/poll.py | 32 +---- src/unipoll_api/actions/workspace.py | 88 +----------- src/unipoll_api/routes/group.py | 63 ++++++--- src/unipoll_api/routes/poll.py | 13 +- src/unipoll_api/routes/workspace.py | 62 ++++++--- 7 files changed, 181 insertions(+), 363 deletions(-) diff --git a/src/unipoll_api/actions/group.py b/src/unipoll_api/actions/group.py index 5b36c47..03191ca 100644 --- a/src/unipoll_api/actions/group.py +++ b/src/unipoll_api/actions/group.py @@ -1,11 +1,9 @@ from beanie import DeleteRules, WriteRules -from beanie.operators import In from unipoll_api import AccountManager -from unipoll_api.documents import Policy, ResourceID, Workspace, Group, Account, create_link +from unipoll_api.documents import Policy, Workspace, Group, Account, create_link from unipoll_api import actions -from unipoll_api.schemas import AccountSchemas, GroupSchemas, MemberSchemas, PolicySchemas, WorkspaceSchemas -from unipoll_api.exceptions import (AccountExceptions, GroupExceptions, PolicyExceptions, - ResourceExceptions, WorkspaceExceptions) +from unipoll_api.schemas import GroupSchemas, WorkspaceSchemas +from unipoll_api.exceptions import (GroupExceptions, WorkspaceExceptions) from unipoll_api.utils import Permissions @@ -108,7 +106,7 @@ async def get_group(group: Group, include_members: bool = False, include_policie account, group, f"to view group {group.id}") members = (await actions.MembersActions.get_members(group)).members if include_members else None - policies = (await get_group_policies(group)).policies if include_policies else None + policies = (await actions.PolicyActions.get_policies(resource=group)).policies if include_policies else None workspace = WorkspaceSchemas.Workspace(**group.workspace.model_dump(exclude={"members", # type: ignore "policies", "groups"})) @@ -163,190 +161,3 @@ async def delete_group(group: Group): if await Group.get(group.id): return GroupExceptions.ErrorWhileDeleting(group.id) - - -# # Get list of members of a group -# async def get_group_members(group: Group) -> MemberSchemas.MemberList: -# member_list = [] -# member: Account - -# account = AccountManager.active_user.get() -# permissions = await Permissions.get_all_permissions(group, account) -# # type: ignore -# req_permissions = Permissions.GroupPermissions["get_group_members"] -# if Permissions.check_permission(permissions, req_permissions): -# for member in group.members: # type: ignore -# member_data = member.model_dump( -# include={'id', 'first_name', 'last_name', 'email'}) -# member_scheme = MemberSchemas.Member(**member_data) -# member_list.append(member_scheme) -# # Return the list of members -# return MemberSchemas.MemberList(members=member_list) - - -# async def add_member(group: Group, account: "Account", permissions, save: bool = True) -> "Account": -# if account.id not in [member.id for member in self.workspace.members]: # type: ignore -# raise Exceptions.WorkspaceExceptions.UserNotMember(self.workspace, account) # type: ignore -# # Add the account to the group -# self.members.append(account) # type: ignore -# # Create a policy for the new member -# new_policy = Policy(policy_holder_type='account', -# policy_holder=(await create_link(account)), -# permissions=permissions, -# parent_resource=self) # type: ignore - -# # Add the policy to the group -# self.policies.append(new_policy) # type: ignore -# if save: -# await self.save(link_rule=WriteRules.WRITE) # type: ignore -# return account - - -# async def remove_member(self, account, save: bool = True) -> bool: -# # Remove the account from the group -# for i, member in enumerate(self.members): -# if account.id == member.id: # type: ignore -# self.members.remove(member) -# Debug.info(f"Removed member {member.id} from {self.resource_type} {self.id}") # type: ignore -# break - -# # Remove the policy from the group -# for policy in self.policies: -# pc = policy.policy_holder # type: ignore -# if pc.ref.id == account.id: -# self.policies.remove(policy) -# await Policy.delete(policy) -# Debug.info(f"Removed policy: {pc.ref.id} from {self.resource_type} {self.id}") -# break - -# if save: -# await self.save(link_rule=WriteRules.WRITE) # type: ignore -# return True - - - - - - -# Get all policies of a group -async def get_group_policies(group: Group) -> PolicySchemas.PolicyList: - policy_list = [] - policy: Policy - account = AccountManager.active_user.get() - permissions = await Permissions.get_all_permissions(group, account) - # type: ignore - req_permissions = Permissions.GroupPermissions["get_group_policies"] - if Permissions.check_permission(permissions, req_permissions): - for policy in group.policies: # type: ignore - permissions = Permissions.GroupPermissions( - policy.permissions).name.split('|') # type: ignore - # Get the policy_holder - if policy.policy_holder_type == 'account': - policy_holder = await Account.get(policy.policy_holder.ref.id) - elif policy.policy_holder_type == 'group': - policy_holder = await Group.get(policy.policy_holder.ref.id) - else: - raise ResourceExceptions.InternalServerError( - "Invalid policy_holder_type") - if not policy_holder: - # TODO: Replace with custom exception - raise ResourceExceptions.InternalServerError( - "get_group_policies() => Policy holder not found") - # Convert the policy_holder to a Member schema - policy_holder = MemberSchemas.Member( - **policy_holder.model_dump()) # type: ignore - policy_list.append(PolicySchemas.PolicyShort(id=policy.id, - policy_holder_type=policy.policy_holder_type, - # Exclude unset fields(i.e. "description" for Account) - policy_holder=policy_holder.model_dump( - exclude_unset=True), - permissions=permissions)) - return PolicySchemas.PolicyList(policies=policy_list) - - -# List all permissions for a user in a workspace -async def get_group_policy(group: Group, account_id: ResourceID | None): - # Check if account_id is specified in request, if account_id is not specified, use the current user - if account_id: - account = await Account.get(account_id) # type: ignore - if not account: - raise AccountExceptions.AccountNotFound(account_id) - else: - account = AccountManager.active_user.get() - - if not account: - 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]: - if account not in group.members: - raise GroupExceptions.UserNotMember(group, account) - - # await group.fetch_link(Group.policies) - user_permissions = await Permissions.get_all_permissions(group, account) - res = {'permissions': Permissions.GroupPermissions(user_permissions).name.split('|'), # type: ignore - 'account': AccountSchemas.AccountShort(**account.model_dump())} - return res - - -async def set_group_policy(group: Group, - input_data: PolicySchemas.PolicyInput) -> PolicySchemas.PolicyOutput: - policy: Policy | None = None - account: Account | None = None - if input_data.policy_id: - policy = await Policy.get(input_data.policy_id) - if not policy: - raise PolicyExceptions.PolicyNotFound(input_data.policy_id) - # BUG: Beanie cannot fetch policy_holder link, as it can be a Group or an Account - else: - account = await Account.get(policy.policy_holder.ref.id) - else: - if input_data.account_id: - account = await Account.get(input_data.account_id) - if not account: - raise AccountExceptions.AccountNotFound(input_data.account_id) - else: - account = AccountManager.active_user.get() - # Make sure the account is loaded - if not account: - 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 - p: Policy - for p in group.policies: # type: ignore - if p.policy_holder_type == "account": - if p.policy_holder.ref.id == account.id: - policy = p - break - except Exception as e: - raise ResourceExceptions.InternalServerError(str(e)) - # Calculate the new permission value - new_permission_value = 0 - for i in input_data.permissions: - try: - # type: ignore - new_permission_value += Permissions.GroupPermissions[i].value - except KeyError: - raise ResourceExceptions.InvalidPermission(i) - # Update the policy - policy.permissions = Permissions.GroupPermissions( - new_permission_value) # type: ignore - await Policy.save(policy) - - # Get Account or Group from policy_holder link - # HACK: Have to do it manualy, as Beanie cannot fetch policy_holder link of mixed types (Account | Group) - if policy.policy_holder_type == "account": # type: ignore - # type: ignore - policy_holder = await Account.get(policy.policy_holder.ref.id) - elif policy.policy_holder_type == "group": # type: ignore - # type: ignore - policy_holder = await Group.get(policy.policy_holder.ref.id) - - # Return the updated policy - return PolicySchemas.PolicyOutput( - permissions=Permissions.GroupPermissions( - policy.permissions).name.split('|'), # type: ignore - policy_holder=MemberSchemas.Member(**policy_holder.model_dump())) # type: ignore diff --git a/src/unipoll_api/actions/policy.py b/src/unipoll_api/actions/policy.py index 589dcdb..df2a672 100644 --- a/src/unipoll_api/actions/policy.py +++ b/src/unipoll_api/actions/policy.py @@ -1,7 +1,7 @@ from unipoll_api import AccountManager from unipoll_api.documents import Account, Workspace, Group, Policy, Resource from unipoll_api.schemas import MemberSchemas, PolicySchemas -from unipoll_api.exceptions import PolicyExceptions +from unipoll_api.exceptions import PolicyExceptions, ResourceExceptions from unipoll_api.utils import Permissions @@ -14,6 +14,27 @@ async def get_policies(policy_holder: Account | Group | None = None, account: Account = AccountManager.active_user.get() all_policies = [] + # Less efficient way of getting policies + # + # search_filter = {} + # if policy_holder: + # search_filter['policy_holder.ref'] = policy_holder.ref + # if resource: + # search_filter['parent_resource.ref'] = resource.ref + # all_policies = await Policy.find(search_filter).to_list() + # for policy in all_policies: + # permissions = await Permissions.get_all_permissions(policy.parent_resource, account) + # if policy.parent_resource.resource_type == "workspace": + # req_permissions = Permissions.WorkspacePermissions["get_workspace_policies"] + # elif policy.parent_resource.resource_type == "group": + # req_permissions = Permissions.GroupPermissions["get_group_policies"] + # elif policy.parent_resource.resource_type == "poll": + # req_permissions = Permissions.PollPermissions["get_poll_policies"] + # if Permissions.check_permission(permissions, req_permissions): + # elif policy_holder.id == policy.policy_holder.ref.id: + # policy_list.append(await get_policy(policy)) + # policy_list.append(await get_policy(policy)) + # Helper function to get policies from a resource async def get_policies_from_resource(resource: Resource) -> list[Policy]: req_permissions: Permissions.Permissions | None = None @@ -25,6 +46,9 @@ async def get_policies_from_resource(resource: Resource) -> list[Policy]: permissions = await Permissions.get_all_permissions(resource, account) if Permissions.check_permission(permissions, req_permissions): return resource.policies # type: ignore + else: + user_policy = await Policy.find_one({"policy_holder.ref": account.ref, "parent_resource.ref": resource.ref}) + return [user_policy] if user_policy else [] return [] # Get policies from a specific resource @@ -51,6 +75,8 @@ async def get_policies_from_resource(resource: Resource) -> list[Policy]: async def get_policy(policy: Policy) -> PolicySchemas.PolicyShort: + # NOTE: Alternatevely, we can check here if the user has the required permissions to get the policy + # Convert policy_holder link to Member object ph_type = policy.policy_holder_type ph_ref = policy.policy_holder.ref.id @@ -60,20 +86,61 @@ async def get_policy(policy: Policy) -> PolicySchemas.PolicyShort: raise PolicyExceptions.PolicyHolderNotFound(ph_ref) policy_holder = MemberSchemas.Member(**policy_holder.model_dump()) # type: ignore - permissions = Permissions.WorkspacePermissions(policy.permissions).name.split('|') # type: ignore + if policy.parent_resource.resource_type == "workspace": # type: ignore + PermissionType = Permissions.WorkspacePermissions + elif policy.parent_resource.resource_type == "group": # type: ignore + PermissionType = Permissions.GroupPermissions + elif policy.parent_resource.resource_type == "poll": # type: ignore + PermissionType = Permissions.PollPermissions + else: + raise ResourceExceptions.InternalServerError("Unknown resource type") + + permissions = PermissionType(policy.permissions).name.split('|') # type: ignore return PolicySchemas.PolicyShort(id=policy.id, policy_holder_type=policy.policy_holder_type, policy_holder=policy_holder.model_dump(exclude_unset=True), permissions=permissions) - # if not account and account_id: - # raise AccountExceptions.AccountNotFound(account_id) - # # Check if account is a member of the workspace - # if account.id not in [member.id for member in workspace.members]: # type: ignore - # raise WorkspaceExceptions.UserNotMember(workspace, account) +async def update_policy(policy: Policy, new_permissions: list[str]) -> PolicySchemas.PolicyOutput: + + # BUG: since the parent_resource is of multiple types, it is not fetched properly, so we fetch it manually + await policy.parent_resource.fetch_all_links() # type: ignore + + # Check if the user has the required permissions to update the policy + account: Account = AccountManager.active_user.get() + permissions = await Permissions.get_all_permissions(policy.parent_resource, account) + if policy.parent_resource.resource_type == "workspace": # type: ignore + ResourcePermissions = Permissions.WorkspacePermissions + req_permissions = Permissions.WorkspacePermissions["set_workspace_policy"] + elif policy.parent_resource.resource_type == "group": # type: ignore + ResourcePermissions = Permissions.GroupPermissions + req_permissions = Permissions.GroupPermissions["set_group_policy"] + elif policy.parent_resource.resource_type == "poll": # type: ignore + ResourcePermissions = Permissions.PollPermissions + req_permissions = Permissions.PollPermissions["set_poll_policy"] + else: + raise ResourceExceptions.InternalServerError("Unknown resource type") + + if not Permissions.check_permission(permissions, req_permissions): + raise ResourceExceptions.UserNotAuthorized(account, "policy", "Update policy") + + # Calculate the new permission value from request + new_permission_value = 0 + for i in new_permissions: + try: + new_permission_value += ResourcePermissions[i].value # type: ignore + except KeyError: + raise ResourceExceptions.InvalidPermission(i) + # Update permissions + policy.permissions = ResourcePermissions(new_permission_value) # type: ignore + await Policy.save(policy) + + if policy.policy_holder_type == "account": + policy_holder = await Account.get(policy.policy_holder.ref.id) + elif policy.policy_holder_type == "group": + policy_holder = await Group.get(policy.policy_holder.ref.id) - # user_permissions = await Permissions.get_all_permissions(workspace, account) - # return PolicySchemas.PolicyOutput( - # permissions=Permissions.WorkspacePermissions(user_permissions).name.split('|'), # type: ignore - # policy_holder=MemberSchemas.Member(**account.model_dump())) + return PolicySchemas.PolicyOutput( + permissions=ResourcePermissions(policy.permissions).name.split('|'), # type: ignore + policy_holder=policy_holder.model_dump()) # type: ignore diff --git a/src/unipoll_api/actions/poll.py b/src/unipoll_api/actions/poll.py index 121fb2d..7697e07 100644 --- a/src/unipoll_api/actions/poll.py +++ b/src/unipoll_api/actions/poll.py @@ -1,8 +1,9 @@ from unipoll_api import AccountManager -from unipoll_api.documents import Poll, Policy, Group, Account, Workspace -from unipoll_api.schemas import PollSchemas, QuestionSchemas, PolicySchemas, MemberSchemas, WorkspaceSchemas +from unipoll_api.documents import Poll, Workspace +from unipoll_api.schemas import PollSchemas, QuestionSchemas, WorkspaceSchemas from unipoll_api.utils import Permissions from unipoll_api.exceptions import ResourceExceptions +from unipoll_api import actions async def get_polls(workspace: Workspace | None = None) -> PollSchemas.PollList: @@ -46,7 +47,7 @@ async def get_poll(poll: Poll, 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 + policies = (await actions.PolicyActions.get_policies(resource=poll)).policies workspace = WorkspaceSchemas.WorkspaceShort(**poll.workspace.model_dump()) # type: ignore @@ -72,31 +73,6 @@ async def get_poll_questions(poll: Poll) -> QuestionSchemas.QuestionList: return QuestionSchemas.QuestionList(questions=question_list) -async def get_poll_policies(poll: Poll) -> PolicySchemas.PolicyList: - policy_list = [] - policy: Policy - for policy in poll.policies: # type: ignore - permissions = Permissions.pollPermissions(policy.permissions).name.split('|') # type: ignore - # Get the policy_holder - if policy.policy_holder_type == 'account': - policy_holder = await Account.get(policy.policy_holder.ref.id) - elif policy.policy_holder_type == 'group': - policy_holder = await Group.get(policy.policy_holder.ref.id) - else: - raise ResourceExceptions.InternalServerError("Invalid policy_holder_type") - if not policy_holder: - # TODO: Replace with custom exception - raise ResourceExceptions.InternalServerError("get_poll_policies() => Policy holder not found") - # Convert the policy_holder to a Member schema - policy_holder = MemberSchemas.Member(**policy_holder.model_dump()) # type: ignore - policy_list.append(PolicySchemas.PolicyShort(id=policy.id, - policy_holder_type=policy.policy_holder_type, - # Exclude unset fields(i.e. "description" for Account) - policy_holder=policy_holder.model_dump(exclude_unset=True), - permissions=permissions)) - return PolicySchemas.PolicyList(policies=policy_list) - - async def update_poll(poll: Poll, data: PollSchemas.UpdatePollRequest) -> PollSchemas.PollResponse: # Update the poll if data.name: diff --git a/src/unipoll_api/actions/workspace.py b/src/unipoll_api/actions/workspace.py index 9cc10c3..795c5b8 100644 --- a/src/unipoll_api/actions/workspace.py +++ b/src/unipoll_api/actions/workspace.py @@ -54,7 +54,7 @@ async def get_workspace(workspace: Workspace, include_polls: bool = False) -> WorkspaceSchemas.Workspace: groups = (await actions.GroupActions.get_groups(workspace)).groups if include_groups else None members = (await actions.MembersActions.get_members(workspace)).members if include_members else None - policies = (await get_workspace_policies(workspace)).policies if include_policies else None + policies = (await actions.PolicyActions.get_policies(resource=workspace)).policies if include_policies else None polls = (await get_polls(workspace)).polls if include_polls else None # Return the workspace with the fetched resources return WorkspaceSchemas.Workspace(id=workspace.id, @@ -98,92 +98,6 @@ async def delete_workspace(workspace: Workspace): await Group.find(Group.workspace.id == workspace).delete() # type: ignore - - - -# Get all policies of a workspace -async def get_workspace_policies(workspace: Workspace) -> PolicySchemas.PolicyList: - policy_list = await actions.PolicyActions.get_policies(resource=workspace) - - return PolicySchemas.PolicyList(policies=policy_list.policies) - - -# Get a policy of a workspace -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 AccountManager.active_user.get() # type: ignore - policy_list = await actions.PolicyActions.get_policies(resource=workspace, policy_holder=account) - user_policy = policy_list.policies[0] - - return PolicySchemas.PolicyOutput( - permissions=user_policy.permissions, # type: ignore - policy_holder=user_policy.policy_holder) - - -# Set permissions for a user in a workspace -async def set_workspace_policy(workspace: Workspace, - input_data: PolicySchemas.PolicyInput) -> PolicySchemas.PolicyOutput: - policy: Policy | None = None - account: Account | None = None - if input_data.policy_id: - policy = await Policy.get(input_data.policy_id) - if not policy: - raise PolicyExceptions.PolicyNotFound(input_data.policy_id) - # BUG: Beanie cannot fetch policy_holder link, as it can be a Group or an Account - else: - account = await Account.get(policy.policy_holder.ref.id) - else: - if input_data.account_id: - account = await Account.get(input_data.account_id) - if not account: - raise AccountExceptions.AccountNotFound(input_data.account_id) - else: - account = AccountManager.active_user.get() - # Make sure the account is loaded - if not account: - raise ResourceExceptions.APIException(code=500, detail='Unknown error') # Should not happen - - try: - # Find the policy for the account - p: Policy - for p in workspace.policies: # type: ignore - if p.policy_holder_type == "account": - if p.policy_holder.ref.id == account.id: - policy = p - break - # if not policy: - # policy = Policy(policy_holder_type='account', - # policy_holder=(await create_link(account)), - # permissions=Permissions.WorkspacePermissions(0), - # workspace=workspace) - except Exception as e: - raise ResourceExceptions.InternalServerError(str(e)) - - # Calculate the new permission value from request - new_permission_value = 0 - for i in input_data.permissions: - try: - new_permission_value += Permissions.WorkspacePermissions[i].value # type: ignore - except KeyError: - raise ResourceExceptions.InvalidPermission(i) - # Update permissions - policy.permissions = Permissions.WorkspacePermissions(new_permission_value) # type: ignore - await Policy.save(policy) - - # Get Account or Group from policy_holder link - # HACK: Have to do it manualy, as Beanie cannot fetch policy_holder link of mixed types (Account | Group) - if policy.policy_holder_type == "account": # type: ignore - policy_holder = await Account.get(policy.policy_holder.ref.id) # type: ignore - elif policy.policy_holder_type == "group": # type: ignore - policy_holder = await Group.get(policy.policy_holder.ref.id) # type: ignore - - # Return the updated policy - return PolicySchemas.PolicyOutput( - permissions=Permissions.WorkspacePermissions(policy.permissions).name.split('|'), # type: ignore - policy_holder=MemberSchemas.Member(**policy_holder.model_dump())) # type: ignore - - # Get a list of polls in a workspace async def get_polls(workspace: Workspace) -> PollSchemas.PollList: return await actions.PollActions.get_polls(workspace) diff --git a/src/unipoll_api/routes/group.py b/src/unipoll_api/routes/group.py index 4b53549..de094f0 100644 --- a/src/unipoll_api/routes/group.py +++ b/src/unipoll_api/routes/group.py @@ -1,11 +1,12 @@ # FastAPI from typing import Annotated, Literal -from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, status +from fastapi import APIRouter, Body, Depends, HTTPException, Query, status from unipoll_api import dependencies as Dependencies -from unipoll_api.actions import GroupActions, PermissionsActions, MembersActions +from unipoll_api import AccountManager +from unipoll_api.actions import GroupActions, PermissionsActions, MembersActions, PolicyActions from unipoll_api.exceptions.resource import APIException from unipoll_api.schemas import GroupSchemas, PolicySchemas, MemberSchemas -from unipoll_api.documents import Account, Group, ResourceID +from unipoll_api.documents import Account, Group, Policy, ResourceID # APIRouter creates path operations for user module @@ -20,7 +21,7 @@ async def get_all_groups(workspace: Annotated[ResourceID | None, Query()] = None name: Annotated[str | None, Query()] = None ) -> GroupSchemas.GroupList: args = {} - args['workspace'] = await Dependencies.get_workspace_model(workspace) if workspace else None + args['workspace'] = await Dependencies.get_workspace(workspace) if workspace else None args['account'] = await Dependencies.get_account(account) if account else None args['name'] = name @@ -34,7 +35,7 @@ async def get_all_groups(workspace: Annotated[ResourceID | None, Query()] = None response_model=GroupSchemas.GroupCreateOutput) async def create_group(input_data: GroupSchemas.GroupCreateInput = Body(...)): try: - workspace = await Dependencies.get_workspace_model(input_data.workspace) + workspace = await Dependencies.get_workspace(input_data.workspace) return await GroupActions.create_group(workspace, name=input_data.name, description=input_data.description) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -49,7 +50,7 @@ async def create_group(input_data: GroupSchemas.GroupCreateInput = Body(...)): response_model=GroupSchemas.Group, response_model_exclude_defaults=True, response_model_exclude_none=True) -async def get_group(group: Group = Depends(Dependencies.get_group_model), +async def get_group(group: Group = Depends(Dependencies.get_group), include: Annotated[query_params | None, Query()] = None): try: params = {} @@ -71,7 +72,7 @@ async def get_group(group: Group = Depends(Dependencies.get_group_model), response_description="Update a group", response_model=GroupSchemas.GroupShort) async def update_group(group_data: GroupSchemas.GroupUpdateRequest, - group: Group = Depends(Dependencies.get_group_model)): + group: Group = Depends(Dependencies.get_group)): try: return await GroupActions.update_group(group, group_data) except APIException as e: @@ -82,7 +83,7 @@ async def update_group(group_data: GroupSchemas.GroupUpdateRequest, @router.delete("/{group_id}", status_code=status.HTTP_204_NO_CONTENT, response_description="Delete a group") -async def delete_group(group: Group = Depends(Dependencies.get_group_model)): +async def delete_group(group: Group = Depends(Dependencies.get_group)): try: await GroupActions.delete_group(group) return status.HTTP_204_NO_CONTENT @@ -95,7 +96,7 @@ async def delete_group(group: Group = Depends(Dependencies.get_group_model)): response_description="List of group members", response_model=MemberSchemas.MemberList, response_model_exclude_unset=True) -async def get_group_members(group: Group = Depends(Dependencies.get_group_model)): +async def get_group_members(group: Group = Depends(Dependencies.get_group)): try: return await MembersActions.get_members(group) except APIException as e: @@ -107,7 +108,7 @@ async def get_group_members(group: Group = Depends(Dependencies.get_group_model) response_description="List of group members", response_model=MemberSchemas.MemberList) async def add_group_members(member_data: MemberSchemas.AddMembers, - group: Group = Depends(Dependencies.get_group_model)): + group: Group = Depends(Dependencies.get_group)): try: return await MembersActions.add_members(group, member_data.accounts) except APIException as e: @@ -118,9 +119,8 @@ async def add_group_members(member_data: MemberSchemas.AddMembers, @router.delete("/{group_id}/members/{account_id}", response_description="Updated list removed members", response_model_exclude_unset=True) -async def remove_group_member(group: Group = Depends(Dependencies.get_group_model), - account: Account = Depends(Dependencies.get_account)): - # account_id: ResourceID = Path(..., description="Account ID of the member to remove")): +async def remove_group_member(group: Group = Depends(Dependencies.get_group), + account: Account = Depends(Dependencies.get_account)): try: return await MembersActions.remove_member(group, account) except APIException as e: @@ -131,9 +131,9 @@ async def remove_group_member(group: Group = Depends(Dependencies.get_group_mode @router.get("/{group_id}/policies", response_description="List of all policies", response_model=PolicySchemas.PolicyList,) -async def get_group_policies(group: Group = Depends(Dependencies.get_group_model)) -> PolicySchemas.PolicyList: +async def get_group_policies(group: Group = Depends(Dependencies.get_group)) -> PolicySchemas.PolicyList: try: - return await GroupActions.get_group_policies(group) + return await PolicyActions.get_policies(resource=group) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -142,10 +142,13 @@ async def get_group_policies(group: Group = Depends(Dependencies.get_group_model @router.get("/{group_id}/policy", response_description="List of all member policies", response_model=PolicySchemas.PolicyOutput) -async def get_group_policy(group: Group = Depends(Dependencies.get_group_model), +async def get_group_policy(group: Group = Depends(Dependencies.get_group), account_id: ResourceID | None = None): try: - return await GroupActions.get_group_policy(group, account_id) + account = await Dependencies.get_account(account_id) if account_id else AccountManager.active_user.get() + policy_list = await PolicyActions.get_policies(resource=group, policy_holder=account) + policy = policy_list.policies[0] + return PolicySchemas.PolicyOutput(**policy.model_dump()) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -154,7 +157,7 @@ async def get_group_policy(group: Group = Depends(Dependencies.get_group_model), @router.put("/{group_id}/policy", response_description="Updated policy", response_model=PolicySchemas.PolicyOutput) -async def set_group_policy(group: Group = Depends(Dependencies.get_group_model), +async def set_group_policy(group: Group = Depends(Dependencies.get_group), permissions: PolicySchemas.PolicyInput = Body(...)): """ Sets the permissions for a user in a workspace. @@ -165,7 +168,29 @@ async def set_group_policy(group: Group = Depends(Dependencies.get_group_model), - **permissions** (int): new permissions for the user """ try: - return await GroupActions.set_group_policy(group, permissions) + # return await GroupActions.set_group_policy(group, permissions) + + policy = None + if permissions.policy_id: + policy = await Dependencies.get_policy(permissions.policy_id) # type: ignore + elif permissions.account_id: + account = await Dependencies.get_account(permissions.account_id) + # policy = await Policy.find_one(Policy.policy_holder.id == account.id, fetch_links=True) + # Temporarily workaround + policy_list = await PolicyActions.get_policies(resource=group, policy_holder=account) + policy = policy_list.policies[0] + policy = await Policy.get(policy.id, fetch_links=True) + elif permissions.group_id: + # Temporarily workaround + group = await Dependencies.get_group(permissions.group_id) + policy_list = await PolicyActions.get_policies(resource=group, policy_holder=group) + policy = policy_list.policies[0] + policy = await Policy.get(policy.id, fetch_links=True) + + if not policy: + raise APIException(404, "Policy not found 404") + + return await PolicyActions.update_policy(policy, new_permissions=permissions.permissions) 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 f9431bf..613ebb9 100644 --- a/src/unipoll_api/routes/poll.py +++ b/src/unipoll_api/routes/poll.py @@ -7,6 +7,7 @@ from unipoll_api.exceptions.resource import APIException from unipoll_api.actions import PollActions from unipoll_api.schemas import PollSchemas, QuestionSchemas, PolicySchemas +from unipoll_api import actions open_router: APIRouter = APIRouter() router: APIRouter = APIRouter(dependencies=[Depends(Dependencies.check_poll_permission)]) @@ -20,7 +21,7 @@ response_description="Poll details", response_model=PollSchemas.PollResponse, response_model_exclude_none=True) -async def get_poll(poll: Poll = Depends(Dependencies.get_poll_model), +async def get_poll(poll: Poll = Depends(Dependencies.get_poll), include: Annotated[query_params | None, Query()] = None): try: params = {} @@ -42,7 +43,7 @@ async def get_poll(poll: Poll = Depends(Dependencies.get_poll_model), response_description="Update Poll detail", response_model=PollSchemas.PollResponse, response_model_exclude_none=True) -async def update_poll(poll: Poll = Depends(Dependencies.get_poll_model), +async def update_poll(poll: Poll = Depends(Dependencies.get_poll), data: PollSchemas.UpdatePollRequest = Body(...)): try: return await PollActions.update_poll(poll, data) @@ -54,7 +55,7 @@ async def update_poll(poll: Poll = Depends(Dependencies.get_poll_model), @router.delete("/{poll_id}", response_description="Result of delete operation", status_code=204) -async def delete_poll(poll: Poll = Depends(Dependencies.get_poll_model)): +async def delete_poll(poll: Poll = Depends(Dependencies.get_poll)): try: return await PollActions.delete_poll(poll) except APIException as e: @@ -66,7 +67,7 @@ async def delete_poll(poll: Poll = Depends(Dependencies.get_poll_model)): response_description="Questions in a poll", response_model=QuestionSchemas.QuestionList, response_model_exclude_none=True) -async def get_questions(poll: Poll = Depends(Dependencies.get_poll_model), +async def get_questions(poll: Poll = Depends(Dependencies.get_poll), include: Annotated[query_params | None, Query()] = None): try: return await PollActions.get_poll_questions(poll) @@ -78,9 +79,9 @@ async def get_questions(poll: Poll = Depends(Dependencies.get_poll_model), response_description="Policy list of a poll", response_model=PolicySchemas.PolicyList, response_model_exclude_none=True) -async def get_policies(poll: Poll = Depends(Dependencies.get_poll_model), +async def get_policies(poll: Poll = Depends(Dependencies.get_poll), include: Annotated[query_params | None, Query()] = None): try: - return await PollActions.get_poll_policies(poll) + return await actions.PolicyActions.get_policies(resource=poll) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) diff --git a/src/unipoll_api/routes/workspace.py b/src/unipoll_api/routes/workspace.py index 6c97f59..b243b80 100644 --- a/src/unipoll_api/routes/workspace.py +++ b/src/unipoll_api/routes/workspace.py @@ -2,10 +2,11 @@ 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 WorkspaceActions, PermissionsActions, GroupActions, MembersActions +from unipoll_api.actions import WorkspaceActions, PermissionsActions, GroupActions, MembersActions, PolicyActions from unipoll_api.exceptions.resource import APIException -from unipoll_api.documents import Account, Workspace, ResourceID +from unipoll_api.documents import Account, Workspace, ResourceID, Policy from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, GroupSchemas, MemberSchemas, PollSchemas +from unipoll_api import AccountManager # APIRouter creates path operations for user module open_router: APIRouter = APIRouter() @@ -57,7 +58,7 @@ async def create_workspace(input_data: WorkspaceSchemas.WorkspaceCreateInput = B response_model=WorkspaceSchemas.Workspace, response_model_exclude_defaults=True, response_model_exclude_none=True) -async def get_workspace(workspace: Workspace = Depends(Dependencies.get_workspace_model), +async def get_workspace(workspace: Workspace = Depends(Dependencies.get_workspace), include: Annotated[query_params | None, Query()] = None): """ ### Description: @@ -114,7 +115,7 @@ async def get_workspace(workspace: Workspace = Depends(Dependencies.get_workspac # Update a workspace with the given id @router.patch("/{workspace_id}", response_description="Updated workspace", response_model=WorkspaceSchemas.Workspace) -async def update_workspace(workspace: Workspace = Depends(Dependencies.get_workspace_model), +async def update_workspace(workspace: Workspace = Depends(Dependencies.get_workspace), input_data: WorkspaceSchemas.WorkspaceUpdateRequest = Body(...) ): """ @@ -137,7 +138,7 @@ async def update_workspace(workspace: Workspace = Depends(Dependencies.get_works @router.delete("/{workspace_id}", response_description="Deleted workspace", status_code=204) -async def delete_workspace(workspace: Workspace = Depends(Dependencies.get_workspace_model)): +async def delete_workspace(workspace: Workspace = Depends(Dependencies.get_workspace)): """ Deletes the workspace with the given id. Query parameters: @@ -157,7 +158,7 @@ async def delete_workspace(workspace: Workspace = Depends(Dependencies.get_works @router.get("/{workspace_id}/groups", response_description="List of all groups", response_model=GroupSchemas.GroupList) -async def get_groups(workspace: Workspace = Depends(Dependencies.get_workspace_model)): +async def get_groups(workspace: Workspace = Depends(Dependencies.get_workspace)): try: return await GroupActions.get_groups(workspace) except APIException as e: @@ -169,7 +170,7 @@ async def get_groups(workspace: Workspace = Depends(Dependencies.get_workspace_m status_code=201, response_description="Created Group", response_model=GroupSchemas.GroupCreateOutput) -async def create_group(workspace: Workspace = Depends(Dependencies.get_workspace_model), +async def create_group(workspace: Workspace = Depends(Dependencies.get_workspace), input_data: GroupSchemas.GroupCreateInput = Body(...)): try: return await GroupActions.create_group(workspace, input_data.name, input_data.description) @@ -182,7 +183,7 @@ async def create_group(workspace: Workspace = Depends(Dependencies.get_workspace response_description="List of all groups", response_model=MemberSchemas.MemberList, response_model_exclude_unset=True) -async def get_workspace_members(workspace: Workspace = Depends(Dependencies.get_workspace_model)): +async def get_workspace_members(workspace: Workspace = Depends(Dependencies.get_workspace)): try: return await MembersActions.get_members(workspace) except APIException as e: @@ -193,7 +194,7 @@ async def get_workspace_members(workspace: Workspace = Depends(Dependencies.get_ @router.post("/{workspace_id}/members", response_description="List added members", response_model=MemberSchemas.MemberList) -async def add_workspace_members(workspace: Workspace = Depends(Dependencies.get_workspace_model), +async def add_workspace_members(workspace: Workspace = Depends(Dependencies.get_workspace), member_data: MemberSchemas.AddMembers = Body(...)): try: return await MembersActions.add_members(workspace, member_data.accounts) @@ -205,9 +206,8 @@ async def add_workspace_members(workspace: Workspace = Depends(Dependencies.get_ @router.delete("/{workspace_id}/members/{account_id}", response_description="Updated list removed members", response_model_exclude_unset=True) -async def remove_workspace_member(workspace: Workspace = Depends(Dependencies.get_workspace_model), +async def remove_workspace_member(workspace: Workspace = Depends(Dependencies.get_workspace), account: Account = Depends(Dependencies.get_account)): - # account_id: ResourceID = Path(..., description="Account ID of the member to remove")): try: return await MembersActions.remove_member(workspace, account) except APIException as e: @@ -218,9 +218,9 @@ async def remove_workspace_member(workspace: Workspace = Depends(Dependencies.ge @router.get("/{workspace_id}/policies", response_description="List of all policies", response_model=PolicySchemas.PolicyList) -async def get_workspace_policies(workspace: Workspace = Depends(Dependencies.get_workspace_model)): +async def get_workspace_policies(workspace: Workspace = Depends(Dependencies.get_workspace)): try: - return await WorkspaceActions.get_workspace_policies(workspace) + return await PolicyActions.get_policies(resource=workspace) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -229,10 +229,13 @@ async def get_workspace_policies(workspace: Workspace = Depends(Dependencies.get @router.get("/{workspace_id}/policy", response_description="List member policy(permissions)", response_model=PolicySchemas.PolicyOutput) -async def get_workspace_policy(workspace: Workspace = Depends(Dependencies.get_workspace_model), +async def get_workspace_policy(workspace: Workspace = Depends(Dependencies.get_workspace), account_id: ResourceID | None = None): try: - return await WorkspaceActions.get_workspace_policy(workspace, account_id) + account = await Dependencies.get_account(account_id) if account_id else AccountManager.active_user.get() + policy_list = await PolicyActions.get_policies(resource=workspace, policy_holder=account) + policy = policy_list.policies[0] + return PolicySchemas.PolicyOutput(**policy.model_dump()) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -241,7 +244,7 @@ async def get_workspace_policy(workspace: Workspace = Depends(Dependencies.get_w @router.put("/{workspace_id}/policy", response_description="Updated permissions", response_model=PolicySchemas.PolicyOutput) -async def set_workspace_policy(workspace: Workspace = Depends(Dependencies.get_workspace_model), +async def set_workspace_policy(workspace: Workspace = Depends(Dependencies.get_workspace), permissions: PolicySchemas.PolicyInput = Body(...)): """ Sets the permissions for a user in a workspace. @@ -254,7 +257,28 @@ async def set_workspace_policy(workspace: Workspace = Depends(Dependencies.get_w Returns the updated workspace. """ try: - return await WorkspaceActions.set_workspace_policy(workspace, permissions) + # return await WorkspaceActions.set_workspace_policy(workspace, permissions) + policy = None + if permissions.policy_id: + policy = await Dependencies.get_policy(permissions.policy_id) # type: ignore + elif permissions.account_id: + account = await Dependencies.get_account(permissions.account_id) + # policy = await Policy.find_one(Policy.policy_holder.id == account.id, fetch_links=True) + # Temporarily workaround + policy_list = await PolicyActions.get_policies(resource=workspace, policy_holder=account) + policy = policy_list.policies[0] + policy = await Policy.get(policy.id, fetch_links=True) + elif permissions.group_id: + # Temporarily workaround + group = await Dependencies.get_group(permissions.group_id) + policy_list = await PolicyActions.get_policies(resource=workspace, policy_holder=group) + policy = policy_list.policies[0] + policy = await Policy.get(policy.id, fetch_links=True) + + if not policy: + raise APIException(404, "Policy not found 404") + + return await PolicyActions.update_policy(policy, new_permissions=permissions.permissions) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -275,7 +299,7 @@ async def get_workspace_permissions(): response_description="List of all polls in the workspace", response_model=PollSchemas.PollList, response_model_exclude_none=True) -async def get_polls(workspace: Workspace = Depends(Dependencies.get_workspace_model)): +async def get_polls(workspace: Workspace = Depends(Dependencies.get_workspace)): try: return await WorkspaceActions.get_polls(workspace) except APIException as e: @@ -287,7 +311,7 @@ async def get_polls(workspace: Workspace = Depends(Dependencies.get_workspace_mo response_description="Created poll", status_code=201, response_model=PollSchemas.PollResponse) -async def create_poll(workspace: Workspace = Depends(Dependencies.get_workspace_model), +async def create_poll(workspace: Workspace = Depends(Dependencies.get_workspace), input_data: PollSchemas.CreatePollRequest = Body(...)): try: return await WorkspaceActions.create_poll(workspace, input_data) From 0bfeb6d07d7ff9fe5646b45db4df4ffa6fa91dc4 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Tue, 10 Oct 2023 23:26:54 -0600 Subject: [PATCH 05/15] feat: Added dependency to get policy by ID --- src/unipoll_api/dependencies.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/unipoll_api/dependencies.py b/src/unipoll_api/dependencies.py index 95b2207..84f0ab4 100644 --- a/src/unipoll_api/dependencies.py +++ b/src/unipoll_api/dependencies.py @@ -1,9 +1,9 @@ from typing import Annotated from fastapi import Cookie, Depends, Query, Request, HTTPException, WebSocket 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.documents import ResourceID, Workspace, Group, Account, Poll, Policy from unipoll_api.utils import permissions as Permissions -from unipoll_api.exceptions import WorkspaceExceptions, GroupExceptions, AccountExceptions, PollExceptions +from unipoll_api.exceptions import WorkspaceExceptions, GroupExceptions, AccountExceptions, PollExceptions, PolicyExceptions from unipoll_api.utils.path_operations import extract_action_from_path, extract_resourceID_from_path @@ -25,7 +25,7 @@ async def websocket_auth(websocket: WebSocket, # Dependency for getting a workspace with the given id -async def get_workspace_model(workspace_id: ResourceID) -> Workspace: +async def get_workspace(workspace_id: ResourceID) -> Workspace: """ Returns a workspace with the given id. """ @@ -38,7 +38,7 @@ async def get_workspace_model(workspace_id: ResourceID) -> Workspace: # Dependency to get a group by id and verify it exists -async def get_group_model(group_id: ResourceID) -> Group: +async def get_group(group_id: ResourceID) -> Group: """ Returns a group with the given id. """ @@ -50,7 +50,7 @@ async def get_group_model(group_id: ResourceID) -> Group: # Dependency to get a poll by id and verify it exists -async def get_poll_model(poll_id: ResourceID) -> Poll: +async def get_poll(poll_id: ResourceID) -> Poll: """ Returns a poll with the given id. """ @@ -60,6 +60,15 @@ async def get_poll_model(poll_id: ResourceID) -> Poll: raise GroupExceptions.GroupNotFound(poll_id) +# Dependency to get a policy by id and verify it exists +async def get_policy(policy_id: ResourceID) -> Policy: + policy = await Policy.get(policy_id, fetch_links=True) + if policy: + # await policy.parent_resource.fetch_all_links() # type: ignore + return policy + raise PolicyExceptions.PolicyNotFound(policy_id) + + # Dependency to get a user by id and verify it exists async def set_active_user(user_account: Account = Depends(get_current_active_user)): active_user.set(user_account) From 504b92f88221599e31c71a82104b7de6f463b6fe Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Tue, 10 Oct 2023 23:31:07 -0600 Subject: [PATCH 06/15] fix: Fixed groups not deleting --- src/unipoll_api/actions/workspace.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/unipoll_api/actions/workspace.py b/src/unipoll_api/actions/workspace.py index 795c5b8..58a4dac 100644 --- a/src/unipoll_api/actions/workspace.py +++ b/src/unipoll_api/actions/workspace.py @@ -1,13 +1,12 @@ # from typing import Optional # from pydantic import EmailStr -from beanie import WriteRules, DeleteRules +from beanie import WriteRules from unipoll_api import AccountManager from unipoll_api import actions -from unipoll_api.documents import Group, ResourceID, Workspace, Account, Policy, Poll +from unipoll_api.documents import Workspace, Account, Policy, Poll from unipoll_api.utils import Permissions -from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, MemberSchemas, PollSchemas -from unipoll_api.exceptions import (WorkspaceExceptions, AccountExceptions, ResourceExceptions, - PolicyExceptions, PollExceptions) +from unipoll_api.schemas import WorkspaceSchemas, PollSchemas +from unipoll_api.exceptions import (WorkspaceExceptions, PollExceptions) # Get a list of workspaces where the account is a owner/member @@ -90,12 +89,14 @@ async def update_workspace(workspace: Workspace, # Delete a workspace async def delete_workspace(workspace: Workspace): - await Workspace.delete(workspace, link_rule=DeleteRules.DO_NOTHING) - # await Workspace.delete(workspace, link_rule=DeleteRules.DELETE_LINKS) + # BUG: Cannot delete groups + for group in workspace.groups: + await actions.GroupActions.delete_group(group) # type: ignore + + await Workspace.delete(workspace) if await workspace.get(workspace.id): raise WorkspaceExceptions.ErrorWhileDeleting(workspace.id) - await Policy.find(Policy.parent_resource.id == workspace.id).delete() # type: ignore - await Group.find(Group.workspace.id == workspace).delete() # type: ignore + await Policy.find({"parent_resource._id": workspace.id}, fetch_links=True).delete() # Get a list of polls in a workspace From a1574808ecc3e8f18e39ce0e7f94ea077a2ac22e Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Tue, 10 Oct 2023 23:31:49 -0600 Subject: [PATCH 07/15] fix: Delete policies when group is deleted --- src/unipoll_api/actions/group.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/unipoll_api/actions/group.py b/src/unipoll_api/actions/group.py index 03191ca..a4b2003 100644 --- a/src/unipoll_api/actions/group.py +++ b/src/unipoll_api/actions/group.py @@ -161,3 +161,5 @@ async def delete_group(group: Group): if await Group.get(group.id): return GroupExceptions.ErrorWhileDeleting(group.id) + + await Policy.find({"parent_resource._id": group.id}, fetch_links=True).delete() From 713dde8426a08b8f0dee34e4da6d3eb21064fcf9 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 11 Oct 2023 13:37:43 -0600 Subject: [PATCH 08/15] style: Cleanup --- src/unipoll_api/actions/group.py | 19 ------------------- src/unipoll_api/actions/members.py | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/unipoll_api/actions/group.py b/src/unipoll_api/actions/group.py index a4b2003..31190e9 100644 --- a/src/unipoll_api/actions/group.py +++ b/src/unipoll_api/actions/group.py @@ -13,10 +13,6 @@ async def get_groups(workspace: Workspace | None = None, name: str | None = None) -> GroupSchemas.GroupList: account = account or AccountManager.active_user.get() - # import time - - # Using Mongo operators - # t1 = time.time() search_filter = {} if name: search_filter['name'] = name @@ -25,21 +21,6 @@ async def get_groups(workspace: Workspace | None = None, if account: search_filter['members._id'] = account.id search_result = await Group.find(search_filter, fetch_links=True).to_list() - # t2 = time.time() - # print("Mongo operator search time: ", t2 - t1) - - # Using Python operators - # t1 = time.time() - # search = Group.find_all(fetch_links=True) - # if name: - # search.find(Group.name == name) - # if workspace: - # search.find(Group.workspace.id == workspace.id, fetch_links=True) - # if account: - # search.find(Group.members.id == account.id, fetch_links=True) - # search_result = await search.to_list() - # t2 = time.time() - # print("Python operator search time: ", t2 - t1) groups = [] for group in search_result: diff --git a/src/unipoll_api/actions/members.py b/src/unipoll_api/actions/members.py index 0d556ca..f1fd2d7 100644 --- a/src/unipoll_api/actions/members.py +++ b/src/unipoll_api/actions/members.py @@ -4,7 +4,7 @@ from unipoll_api.utils import Permissions from unipoll_api.schemas import MemberSchemas from unipoll_api import AccountManager -from unipoll_api.exceptions import ResourceExceptions, AccountExceptions +from unipoll_api.exceptions import ResourceExceptions async def get_members(resource: Workspace | Group) -> MemberSchemas.MemberList: From 2664b7afc3c4ee1f38e6709831f14afa2b4225ee Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 11 Oct 2023 13:38:05 -0600 Subject: [PATCH 09/15] refactor: Moved create poll action --- src/unipoll_api/actions/poll.py | 32 ++++++++++++++++++- src/unipoll_api/actions/workspace.py | 48 +++------------------------- 2 files changed, 36 insertions(+), 44 deletions(-) diff --git a/src/unipoll_api/actions/poll.py b/src/unipoll_api/actions/poll.py index 7697e07..0188f4f 100644 --- a/src/unipoll_api/actions/poll.py +++ b/src/unipoll_api/actions/poll.py @@ -1,8 +1,9 @@ +from beanie import WriteRules from unipoll_api import AccountManager from unipoll_api.documents import Poll, Workspace from unipoll_api.schemas import PollSchemas, QuestionSchemas, WorkspaceSchemas from unipoll_api.utils import Permissions -from unipoll_api.exceptions import ResourceExceptions +from unipoll_api.exceptions import ResourceExceptions, PollExceptions from unipoll_api import actions @@ -31,6 +32,35 @@ async def get_polls(workspace: Workspace | None = None) -> PollSchemas.PollList: return PollSchemas.PollList(polls=poll_list) +# Create a new poll in a workspace +async def create_poll(workspace: Workspace, input_data: PollSchemas.CreatePollRequest) -> PollSchemas.PollResponse: + # Check if poll name is unique + poll: Poll # For type hinting, until Link type is supported + for poll in workspace.polls: # type: ignore + if poll.name == input_data.name: + raise PollExceptions.NonUniqueName(poll) + + # Create a new poll + new_poll = Poll(name=input_data.name, + description=input_data.description, + workspace=workspace, # type: ignore + public=input_data.public, + published=input_data.published, + questions=input_data.questions, + policies=[]) + + # Check if poll was created + if not new_poll: + raise PollExceptions.ErrorWhileCreating(new_poll) + + # Add the poll to the workspace + workspace.polls.append(new_poll) # type: ignore + await Workspace.save(workspace, link_rule=WriteRules.WRITE) + + # Return the new poll + return PollSchemas.PollResponse(**new_poll.model_dump()) + + async def get_poll(poll: Poll, include_questions: bool = False, include_policies: bool = False) -> PollSchemas.PollResponse: diff --git a/src/unipoll_api/actions/workspace.py b/src/unipoll_api/actions/workspace.py index 58a4dac..498823e 100644 --- a/src/unipoll_api/actions/workspace.py +++ b/src/unipoll_api/actions/workspace.py @@ -1,12 +1,9 @@ -# from typing import Optional -# from pydantic import EmailStr -from beanie import WriteRules from unipoll_api import AccountManager from unipoll_api import actions -from unipoll_api.documents import Workspace, Account, Policy, Poll +from unipoll_api.documents import Workspace, Account, Policy from unipoll_api.utils import Permissions -from unipoll_api.schemas import WorkspaceSchemas, PollSchemas -from unipoll_api.exceptions import (WorkspaceExceptions, PollExceptions) +from unipoll_api.schemas import WorkspaceSchemas +from unipoll_api.exceptions import WorkspaceExceptions # Get a list of workspaces where the account is a owner/member @@ -38,8 +35,7 @@ async def create_workspace(input_data: WorkspaceSchemas.WorkspaceCreateInput) -> if not new_workspace: raise WorkspaceExceptions.ErrorWhileCreating(input_data.name) - await new_workspace.add_member(account=account, permissions=Permissions.WORKSPACE_ALL_PERMISSIONS, save=False) - await Workspace.save(new_workspace, link_rule=WriteRules.WRITE) + await new_workspace.add_member(account=account, permissions=Permissions.WORKSPACE_ALL_PERMISSIONS) # Specify fields for output schema return WorkspaceSchemas.WorkspaceCreateOutput(**new_workspace.model_dump(include={'id', 'name', 'description'})) @@ -54,7 +50,7 @@ async def get_workspace(workspace: Workspace, groups = (await actions.GroupActions.get_groups(workspace)).groups if include_groups else None members = (await actions.MembersActions.get_members(workspace)).members if include_members else None policies = (await actions.PolicyActions.get_policies(resource=workspace)).policies if include_policies else None - polls = (await get_polls(workspace)).polls if include_polls else None + polls = (await actions.PollActions.get_polls(workspace)).polls if include_polls else None # Return the workspace with the fetched resources return WorkspaceSchemas.Workspace(id=workspace.id, name=workspace.name, @@ -97,37 +93,3 @@ async def delete_workspace(workspace: Workspace): if await workspace.get(workspace.id): raise WorkspaceExceptions.ErrorWhileDeleting(workspace.id) await Policy.find({"parent_resource._id": workspace.id}, fetch_links=True).delete() - - -# Get a list of polls in a workspace -async def get_polls(workspace: Workspace) -> PollSchemas.PollList: - return await actions.PollActions.get_polls(workspace) - - -# Create a new poll in a workspace -async def create_poll(workspace: Workspace, input_data: PollSchemas.CreatePollRequest) -> PollSchemas.PollResponse: - # Check if poll name is unique - poll: Poll # For type hinting, until Link type is supported - for poll in workspace.polls: # type: ignore - if poll.name == input_data.name: - raise PollExceptions.NonUniqueName(poll) - - # Create a new poll - new_poll = Poll(name=input_data.name, - description=input_data.description, - workspace=workspace, # type: ignore - public=input_data.public, - published=input_data.published, - questions=input_data.questions, - policies=[]) - - # Check if poll was created - if not new_poll: - raise PollExceptions.ErrorWhileCreating(new_poll) - - # Add the poll to the workspace - workspace.polls.append(new_poll) # type: ignore - await Workspace.save(workspace, link_rule=WriteRules.WRITE) - - # Return the new poll - return PollSchemas.PollResponse(**new_poll.model_dump()) From dd7ba5ad23879774949b38e641b09dee6faa5251 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 11 Oct 2023 13:40:44 -0600 Subject: [PATCH 10/15] refactor: Changed imports --- src/unipoll_api/routes/workspace.py | 40 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/unipoll_api/routes/workspace.py b/src/unipoll_api/routes/workspace.py index b243b80..1d2bfd7 100644 --- a/src/unipoll_api/routes/workspace.py +++ b/src/unipoll_api/routes/workspace.py @@ -1,8 +1,8 @@ # FastAPI from typing import Annotated, Literal -from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, status +from fastapi import APIRouter, Body, Depends, HTTPException, Query, status from unipoll_api import dependencies as Dependencies -from unipoll_api.actions import WorkspaceActions, PermissionsActions, GroupActions, MembersActions, PolicyActions +from unipoll_api import actions from unipoll_api.exceptions.resource import APIException from unipoll_api.documents import Account, Workspace, ResourceID, Policy from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, GroupSchemas, MemberSchemas, PollSchemas @@ -24,7 +24,7 @@ async def get_workspaces(): The request does not accept any query parameters. """ try: - return await WorkspaceActions.get_workspaces() + return await actions.WorkspaceActions.get_workspaces() except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -44,7 +44,7 @@ async def create_workspace(input_data: WorkspaceSchemas.WorkspaceCreateInput = B Returns the created workspace information. """ try: - return await WorkspaceActions.create_workspace(input_data=input_data) + return await actions.WorkspaceActions.create_workspace(input_data=input_data) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -107,7 +107,7 @@ async def get_workspace(workspace: Workspace = Depends(Dependencies.get_workspac params["include_policies"] = True if "polls" in include: params["include_polls"] = True - return await WorkspaceActions.get_workspace(workspace, **params) + return await actions.WorkspaceActions.get_workspace(workspace, **params) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -129,7 +129,7 @@ async def update_workspace(workspace: Workspace = Depends(Dependencies.get_works Returns the updated workspace. """ try: - return await WorkspaceActions.update_workspace(workspace, input_data) + return await actions.WorkspaceActions.update_workspace(workspace, input_data) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -148,7 +148,7 @@ async def delete_workspace(workspace: Workspace = Depends(Dependencies.get_works Response has no detail. """ try: - await WorkspaceActions.delete_workspace(workspace) + await actions.WorkspaceActions.delete_workspace(workspace) return status.HTTP_204_NO_CONTENT except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -160,7 +160,7 @@ async def delete_workspace(workspace: Workspace = Depends(Dependencies.get_works response_model=GroupSchemas.GroupList) async def get_groups(workspace: Workspace = Depends(Dependencies.get_workspace)): try: - return await GroupActions.get_groups(workspace) + return await actions.GroupActions.get_groups(workspace) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -173,7 +173,7 @@ async def get_groups(workspace: Workspace = Depends(Dependencies.get_workspace)) async def create_group(workspace: Workspace = Depends(Dependencies.get_workspace), input_data: GroupSchemas.GroupCreateInput = Body(...)): try: - return await GroupActions.create_group(workspace, input_data.name, input_data.description) + return await actions.GroupActions.create_group(workspace, input_data.name, input_data.description) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -185,7 +185,7 @@ async def create_group(workspace: Workspace = Depends(Dependencies.get_workspace response_model_exclude_unset=True) async def get_workspace_members(workspace: Workspace = Depends(Dependencies.get_workspace)): try: - return await MembersActions.get_members(workspace) + return await actions.MembersActions.get_members(workspace) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -197,7 +197,7 @@ async def get_workspace_members(workspace: Workspace = Depends(Dependencies.get_ async def add_workspace_members(workspace: Workspace = Depends(Dependencies.get_workspace), member_data: MemberSchemas.AddMembers = Body(...)): try: - return await MembersActions.add_members(workspace, member_data.accounts) + return await actions.MembersActions.add_members(workspace, member_data.accounts) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -209,7 +209,7 @@ async def add_workspace_members(workspace: Workspace = Depends(Dependencies.get_ async def remove_workspace_member(workspace: Workspace = Depends(Dependencies.get_workspace), account: Account = Depends(Dependencies.get_account)): try: - return await MembersActions.remove_member(workspace, account) + return await actions.MembersActions.remove_member(workspace, account) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -220,7 +220,7 @@ async def remove_workspace_member(workspace: Workspace = Depends(Dependencies.ge response_model=PolicySchemas.PolicyList) async def get_workspace_policies(workspace: Workspace = Depends(Dependencies.get_workspace)): try: - return await PolicyActions.get_policies(resource=workspace) + return await actions.PolicyActions.get_policies(resource=workspace) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -233,7 +233,7 @@ async def get_workspace_policy(workspace: Workspace = Depends(Dependencies.get_w account_id: ResourceID | None = None): try: account = await Dependencies.get_account(account_id) if account_id else AccountManager.active_user.get() - policy_list = await PolicyActions.get_policies(resource=workspace, policy_holder=account) + policy_list = await actions.PolicyActions.get_policies(resource=workspace, policy_holder=account) policy = policy_list.policies[0] return PolicySchemas.PolicyOutput(**policy.model_dump()) except APIException as e: @@ -265,20 +265,20 @@ async def set_workspace_policy(workspace: Workspace = Depends(Dependencies.get_w account = await Dependencies.get_account(permissions.account_id) # policy = await Policy.find_one(Policy.policy_holder.id == account.id, fetch_links=True) # Temporarily workaround - policy_list = await PolicyActions.get_policies(resource=workspace, policy_holder=account) + policy_list = await actions.PolicyActions.get_policies(resource=workspace, policy_holder=account) policy = policy_list.policies[0] policy = await Policy.get(policy.id, fetch_links=True) elif permissions.group_id: # Temporarily workaround group = await Dependencies.get_group(permissions.group_id) - policy_list = await PolicyActions.get_policies(resource=workspace, policy_holder=group) + policy_list = await actions.PolicyActions.get_policies(resource=workspace, policy_holder=group) policy = policy_list.policies[0] policy = await Policy.get(policy.id, fetch_links=True) if not policy: raise APIException(404, "Policy not found 404") - return await PolicyActions.update_policy(policy, new_permissions=permissions.permissions) + return await actions.PolicyActions.update_policy(policy, new_permissions=permissions.permissions) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -289,7 +289,7 @@ async def set_workspace_policy(workspace: Workspace = Depends(Dependencies.get_w response_model=PolicySchemas.PermissionList) async def get_workspace_permissions(): try: - return await PermissionsActions.get_workspace_permissions() + return await actions.PermissionsActions.get_workspace_permissions() except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -301,7 +301,7 @@ async def get_workspace_permissions(): response_model_exclude_none=True) async def get_polls(workspace: Workspace = Depends(Dependencies.get_workspace)): try: - return await WorkspaceActions.get_polls(workspace) + return await actions.PollActions.get_polls(workspace) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) @@ -314,6 +314,6 @@ async def get_polls(workspace: Workspace = Depends(Dependencies.get_workspace)): async def create_poll(workspace: Workspace = Depends(Dependencies.get_workspace), input_data: PollSchemas.CreatePollRequest = Body(...)): try: - return await WorkspaceActions.create_poll(workspace, input_data) + return await actions.PollActions.create_poll(workspace, input_data) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) From 1be8a1a1c02e030eafb61778464eddd344be8c3c Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 11 Oct 2023 13:42:37 -0600 Subject: [PATCH 11/15] style: flake8 linting --- src/unipoll_api/actions/members.py | 2 +- src/unipoll_api/dependencies.py | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/unipoll_api/actions/members.py b/src/unipoll_api/actions/members.py index f1fd2d7..350f2d2 100644 --- a/src/unipoll_api/actions/members.py +++ b/src/unipoll_api/actions/members.py @@ -37,7 +37,7 @@ async def add_members(resource: Workspace | Group, # Check if the user has permission to add members account = AccountManager.active_user.get() permissions = await Permissions.get_all_permissions(resource, account) - if resource.resource_type == "workspace": + if resource.resource_type == "workspace": req_permissions = Permissions.WorkspacePermissions["add_workspace_members"] default_permissions = Permissions.WORKSPACE_BASIC_PERMISSIONS elif resource.resource_type == "group": diff --git a/src/unipoll_api/dependencies.py b/src/unipoll_api/dependencies.py index 84f0ab4..5ca77a6 100644 --- a/src/unipoll_api/dependencies.py +++ b/src/unipoll_api/dependencies.py @@ -3,7 +3,7 @@ from unipoll_api.account_manager import active_user, get_current_active_user from unipoll_api.documents import ResourceID, Workspace, Group, Account, Poll, Policy from unipoll_api.utils import permissions as Permissions -from unipoll_api.exceptions import WorkspaceExceptions, GroupExceptions, AccountExceptions, PollExceptions, PolicyExceptions +from unipoll_api import exceptions as Exceptions from unipoll_api.utils.path_operations import extract_action_from_path, extract_resourceID_from_path @@ -14,7 +14,7 @@ async def get_account(account_id: ResourceID) -> Account: """ account = await Account.get(account_id) if not account: - raise AccountExceptions.AccountNotFound(account_id) + raise Exceptions.AccountExceptions.AccountNotFound(account_id) return account @@ -34,7 +34,7 @@ async def get_workspace(workspace_id: ResourceID) -> Workspace: if workspace: # await workspace.fetch_all_links() return workspace - raise WorkspaceExceptions.WorkspaceNotFound(workspace_id) + raise Exceptions.WorkspaceExceptions.WorkspaceNotFound(workspace_id) # Dependency to get a group by id and verify it exists @@ -46,7 +46,7 @@ async def get_group(group_id: ResourceID) -> Group: if group: # await group.fetch_all_links() return group - raise GroupExceptions.GroupNotFound(group_id) + raise Exceptions.GroupExceptions.GroupNotFound(group_id) # Dependency to get a poll by id and verify it exists @@ -57,7 +57,7 @@ async def get_poll(poll_id: ResourceID) -> Poll: poll = await Poll.get(poll_id, fetch_links=True) if poll: return poll - raise GroupExceptions.GroupNotFound(poll_id) + raise Exceptions.GroupExceptions.GroupNotFound(poll_id) # Dependency to get a policy by id and verify it exists @@ -66,7 +66,7 @@ async def get_policy(policy_id: ResourceID) -> Policy: if policy: # await policy.parent_resource.fetch_all_links() # type: ignore return policy - raise PolicyExceptions.PolicyNotFound(policy_id) + raise Exceptions.PolicyExceptions.PolicyNotFound(policy_id) # Dependency to get a user by id and verify it exists @@ -88,7 +88,7 @@ async def check_workspace_permission(request: Request, account: Account = Depend # Check if workspace exists if not workspace: - e = WorkspaceExceptions.WorkspaceNotFound(workspaceID) + e = Exceptions.WorkspaceExceptions.WorkspaceNotFound(workspaceID) raise HTTPException(e.code, str(e)) if account.is_superuser: @@ -102,10 +102,10 @@ async def check_workspace_permission(request: Request, account: Account = Depend required_permission = Permissions.WorkspacePermissions[operationID] # type: ignore if not Permissions.check_permission(Permissions.WorkspacePermissions(user_permissions), # type: ignore required_permission): - e = WorkspaceExceptions.UserNotAuthorized(account, workspace, operationID) + e = Exceptions.WorkspaceExceptions.UserNotAuthorized(account, workspace, operationID) raise HTTPException(e.code, str(e)) except KeyError: - e = WorkspaceExceptions.ActionNotFound(operationID) + e = Exceptions.WorkspaceExceptions.ActionNotFound(operationID) raise HTTPException(e.code, str(e)) @@ -119,7 +119,7 @@ async def check_group_permission(request: Request, account: Account = Depends(ge # Check if group exists e: Exception if not group: - e = GroupExceptions.GroupNotFound(groupID) + e = Exceptions.GroupExceptions.GroupNotFound(groupID) raise HTTPException(e.code, str(e)) # Get the user policy for the group # print(group.members) @@ -130,10 +130,10 @@ async def check_group_permission(request: Request, account: Account = Depends(ge required_permission = Permissions.GroupPermissions[operationID] # type: ignore if not Permissions.check_permission(Permissions.GroupPermissions(user_permissions), # type: ignore required_permission): - e = GroupExceptions.UserNotAuthorized(account, group, operationID) + e = Exceptions.GroupExceptions.UserNotAuthorized(account, group, operationID) raise HTTPException(e.code, str(e)) except KeyError: - e = GroupExceptions.ActionNotFound(operationID) + e = Exceptions.GroupExceptions.ActionNotFound(operationID) raise HTTPException(e.code, str(e)) @@ -147,7 +147,7 @@ async def check_poll_permission(request: Request, account: Account = Depends(get # Check if poll exists e: Exception if not poll: - e = PollExceptions.PollNotFound(pollID) + e = Exceptions.PollExceptions.PollNotFound(pollID) raise HTTPException(e.code, str(e)) # Check if the poll is public @@ -162,8 +162,8 @@ async def check_poll_permission(request: Request, account: Account = Depends(get required_permission = Permissions.PollPermissions[operationID] # type: ignore if not Permissions.check_permission(Permissions.PollPermissions(user_permissions), # type: ignore required_permission): - e = PollExceptions.UserNotAuthorized(account, poll, operationID) + e = Exceptions.PollExceptions.UserNotAuthorized(account, poll, operationID) raise HTTPException(e.code, str(e)) except KeyError: - e = PollExceptions.ActionNotFound(operationID) + e = Exceptions.PollExceptions.ActionNotFound(operationID) raise HTTPException(e.code, str(e)) From 9a852b3fe3e835ee1e40654fba63f81b3652e8c3 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 11 Oct 2023 19:22:43 -0600 Subject: [PATCH 12/15] fix: Resolved typing errors --- src/unipoll_api/actions/group.py | 6 +++--- src/unipoll_api/actions/members.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/unipoll_api/actions/group.py b/src/unipoll_api/actions/group.py index 31190e9..bb71085 100644 --- a/src/unipoll_api/actions/group.py +++ b/src/unipoll_api/actions/group.py @@ -15,11 +15,11 @@ async def get_groups(workspace: Workspace | None = None, search_filter = {} if name: - search_filter['name'] = name + search_filter['name'] = name # type: ignore if workspace: - search_filter['workspace._id'] = workspace.id + search_filter['workspace._id'] = workspace.id # type: ignore if account: - search_filter['members._id'] = account.id + search_filter['members._id'] = account.id # type: ignore search_result = await Group.find(search_filter, fetch_links=True).to_list() groups = [] diff --git a/src/unipoll_api/actions/members.py b/src/unipoll_api/actions/members.py index 350f2d2..aa1724c 100644 --- a/src/unipoll_api/actions/members.py +++ b/src/unipoll_api/actions/members.py @@ -12,9 +12,9 @@ async def get_members(resource: Workspace | Group) -> MemberSchemas.MemberList: permissions = await Permissions.get_all_permissions(resource, account) if resource.resource_type == "workspace": - req_permissions = Permissions.WorkspacePermissions["get_workspace_members"] + req_permissions = Permissions.WorkspacePermissions["get_workspace_members"] # type: ignore elif resource.resource_type == "group": - req_permissions = Permissions.GroupPermissions["get_group_members"] + req_permissions = Permissions.GroupPermissions["get_group_members"] # type: ignore else: raise ResourceExceptions.InternalServerError("Invalid resource type") @@ -41,8 +41,8 @@ async def add_members(resource: Workspace | Group, req_permissions = Permissions.WorkspacePermissions["add_workspace_members"] default_permissions = Permissions.WORKSPACE_BASIC_PERMISSIONS elif resource.resource_type == "group": - req_permissions = Permissions.GroupPermissions["add_group_members"] - default_permissions = Permissions.GROUP_BASIC_PERMISSIONS + req_permissions = Permissions.GroupPermissions["add_group_members"] # type: ignore + default_permissions = Permissions.GROUP_BASIC_PERMISSIONS # type: ignore else: raise ResourceExceptions.InternalServerError("Invalid resource type") @@ -71,9 +71,9 @@ async def remove_member(resource: Workspace | Group, account: Account): account = AccountManager.active_user.get() permissions = await Permissions.get_all_permissions(resource, account) if resource.resource_type == "workspace": - req_permissions = Permissions.WorkspacePermissions["remove_workspace_members"] + req_permissions = Permissions.WorkspacePermissions["remove_workspace_members"] # type: ignore elif resource.resource_type == "group": - req_permissions = Permissions.GroupPermissions["remove_group_members"] + req_permissions = Permissions.GroupPermissions["remove_group_members"] # type: ignore else: raise ResourceExceptions.InternalServerError("Invalid resource type") if not Permissions.check_permission(permissions, req_permissions): From 924e462373a4d6bd096b8fa04bc4575e35750c37 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 11 Oct 2023 19:24:11 -0600 Subject: [PATCH 13/15] refactor: Add permission check to get_policy --- src/unipoll_api/actions/policy.py | 108 ++++++++++++++---------------- 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/src/unipoll_api/actions/policy.py b/src/unipoll_api/actions/policy.py index df2a672..5c559c5 100644 --- a/src/unipoll_api/actions/policy.py +++ b/src/unipoll_api/actions/policy.py @@ -5,52 +5,36 @@ from unipoll_api.utils import Permissions +# Helper function to get policies from a resource +# NOTE: This can be moved to utils.py +async def get_policies_from_resource(resource: Resource) -> list[Policy]: + account: Account = AccountManager.active_user.get() + req_permissions: Permissions.Permissions | None = None + policies: list[Policy] = [] + if resource.resource_type == "workspace": + req_permissions = Permissions.WorkspacePermissions["get_workspace_policies"] + elif resource.resource_type == "group": + req_permissions = Permissions.GroupPermissions["get_group_policies"] + elif resource.resource_type == "poll": + req_permissions = Permissions.PollPermissions["get_poll_policies"] + if req_permissions: + permissions = await Permissions.get_all_permissions(resource, account) + if Permissions.check_permission(permissions, req_permissions): + policies = resource.policies # type: ignore + else: + for policy in resource.policies: + if policy.policy_holder.ref.id == account.id: # type: ignore + policies.append(policy) # type: ignore + return policies + + # Get all policies of a workspace async def get_policies(policy_holder: Account | Group | None = None, resource: Resource | None = None) -> PolicySchemas.PolicyList: policy_list = [] policy: Policy - - account: Account = AccountManager.active_user.get() all_policies = [] - # Less efficient way of getting policies - # - # search_filter = {} - # if policy_holder: - # search_filter['policy_holder.ref'] = policy_holder.ref - # if resource: - # search_filter['parent_resource.ref'] = resource.ref - # all_policies = await Policy.find(search_filter).to_list() - # for policy in all_policies: - # permissions = await Permissions.get_all_permissions(policy.parent_resource, account) - # if policy.parent_resource.resource_type == "workspace": - # req_permissions = Permissions.WorkspacePermissions["get_workspace_policies"] - # elif policy.parent_resource.resource_type == "group": - # req_permissions = Permissions.GroupPermissions["get_group_policies"] - # elif policy.parent_resource.resource_type == "poll": - # req_permissions = Permissions.PollPermissions["get_poll_policies"] - # if Permissions.check_permission(permissions, req_permissions): - # elif policy_holder.id == policy.policy_holder.ref.id: - # policy_list.append(await get_policy(policy)) - # policy_list.append(await get_policy(policy)) - - # Helper function to get policies from a resource - async def get_policies_from_resource(resource: Resource) -> list[Policy]: - req_permissions: Permissions.Permissions | None = None - if resource.resource_type == "workspace": - req_permissions = Permissions.WorkspacePermissions["get_workspace_policies"] - elif resource.resource_type == "group": - req_permissions = Permissions.GroupPermissions["get_group_policies"] - if req_permissions: - permissions = await Permissions.get_all_permissions(resource, account) - if Permissions.check_permission(permissions, req_permissions): - return resource.policies # type: ignore - else: - user_policy = await Policy.find_one({"policy_holder.ref": account.ref, "parent_resource.ref": resource.ref}) - return [user_policy] if user_policy else [] - return [] - # Get policies from a specific resource if resource: all_policies = await get_policies_from_resource(resource) @@ -62,39 +46,45 @@ async def get_policies_from_resource(resource: Resource) -> list[Policy]: for resource in all_resources: all_policies += await get_policies_from_resource(resource) + # Build policy list for policy in all_policies: # Filter by policy_holder if specified if policy_holder: if (policy.policy_holder.ref.id != policy_holder.id): continue - policy_list.append(await get_policy(policy)) + policy_list.append(await get_policy(policy, False)) # Return policy list return PolicySchemas.PolicyList(policies=policy_list) -async def get_policy(policy: Policy) -> PolicySchemas.PolicyShort: +async def get_policy(policy: Policy, permission_check: bool = True) -> PolicySchemas.PolicyShort: + if permission_check: + account = AccountManager.active_user.get() + permissions = await Permissions.get_all_permissions(policy.parent_resource, account) + resource_type: str = policy.parent_resource.resource_type # type: ignore - # NOTE: Alternatevely, we can check here if the user has the required permissions to get the policy + if resource_type == "workspace": # type: ignore + req_permissions = Permissions.WorkspacePermissions["get_workspace_policies"] # type: ignore + elif resource_type == "group": # type: ignore + req_permissions = Permissions.GroupPermissions["get_group_policies"] # type: ignore + elif resource_type == "poll": # type: ignore + req_permissions = Permissions.PollPermissions["get_poll_policies"] # type: ignore + else: + raise ResourceExceptions.InternalServerError("Unknown resource type") + + if not Permissions.check_permission(permissions, req_permissions): + raise ResourceExceptions.UserNotAuthorized(account, "policy", "Get policy") # Convert policy_holder link to Member object ph_type = policy.policy_holder_type ph_ref = policy.policy_holder.ref.id policy_holder = await Account.get(ph_ref) if ph_type == "account" else await Group.get(ph_ref) - if not policy_holder: raise PolicyExceptions.PolicyHolderNotFound(ph_ref) - policy_holder = MemberSchemas.Member(**policy_holder.model_dump()) # type: ignore - if policy.parent_resource.resource_type == "workspace": # type: ignore - PermissionType = Permissions.WorkspacePermissions - elif policy.parent_resource.resource_type == "group": # type: ignore - PermissionType = Permissions.GroupPermissions - elif policy.parent_resource.resource_type == "poll": # type: ignore - PermissionType = Permissions.PollPermissions - else: - raise ResourceExceptions.InternalServerError("Unknown resource type") - + resource_type: str = policy.parent_resource.resource_type # type: ignore + PermissionType = eval("Permissions." + resource_type.capitalize() + "Permissions") permissions = PermissionType(policy.permissions).name.split('|') # type: ignore return PolicySchemas.PolicyShort(id=policy.id, policy_holder_type=policy.policy_holder_type, @@ -111,14 +101,14 @@ async def update_policy(policy: Policy, new_permissions: list[str]) -> PolicySch account: Account = AccountManager.active_user.get() permissions = await Permissions.get_all_permissions(policy.parent_resource, account) if policy.parent_resource.resource_type == "workspace": # type: ignore - ResourcePermissions = Permissions.WorkspacePermissions - req_permissions = Permissions.WorkspacePermissions["set_workspace_policy"] + ResourcePermissions = Permissions.WorkspacePermissions # type: ignore + req_permissions = Permissions.WorkspacePermissions["set_workspace_policy"] # type: ignore elif policy.parent_resource.resource_type == "group": # type: ignore - ResourcePermissions = Permissions.GroupPermissions - req_permissions = Permissions.GroupPermissions["set_group_policy"] + ResourcePermissions = Permissions.GroupPermissions # type: ignore + req_permissions = Permissions.GroupPermissions["set_group_policy"] # type: ignore elif policy.parent_resource.resource_type == "poll": # type: ignore - ResourcePermissions = Permissions.PollPermissions - req_permissions = Permissions.PollPermissions["set_poll_policy"] + ResourcePermissions = Permissions.PollPermissions # type: ignore + req_permissions = Permissions.PollPermissions["set_poll_policy"] # type: ignore else: raise ResourceExceptions.InternalServerError("Unknown resource type") From 9d9cc2330bf9e3baae61e610aa229ff9c749f1ce Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 11 Oct 2023 19:25:58 -0600 Subject: [PATCH 14/15] fix: Typing errors --- src/unipoll_api/routes/group.py | 23 ++++++++++------------- src/unipoll_api/routes/workspace.py | 8 ++++---- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/unipoll_api/routes/group.py b/src/unipoll_api/routes/group.py index de094f0..781420f 100644 --- a/src/unipoll_api/routes/group.py +++ b/src/unipoll_api/routes/group.py @@ -20,12 +20,9 @@ async def get_all_groups(workspace: Annotated[ResourceID | None, Query()] = None account: Annotated[ResourceID | None, Query()] = None, name: Annotated[str | None, Query()] = None ) -> GroupSchemas.GroupList: - args = {} - args['workspace'] = await Dependencies.get_workspace(workspace) if workspace else None - args['account'] = await Dependencies.get_account(account) if account else None - args['name'] = name - - return await GroupActions.get_groups(**args) + return await GroupActions.get_groups(workspace=await Dependencies.get_workspace(workspace) if workspace else None, + account=await Dependencies.get_account(account) if account else None, + name=name) # Create a new group @@ -33,7 +30,7 @@ async def get_all_groups(workspace: Annotated[ResourceID | None, Query()] = None status_code=201, response_description="Created Group", response_model=GroupSchemas.GroupCreateOutput) -async def create_group(input_data: GroupSchemas.GroupCreateInput = Body(...)): +async def create_group(input_data: GroupSchemas.GroupCreateRequest = Body(...)): try: workspace = await Dependencies.get_workspace(input_data.workspace) return await GroupActions.create_group(workspace, name=input_data.name, description=input_data.description) @@ -177,15 +174,15 @@ async def set_group_policy(group: Group = Depends(Dependencies.get_group), account = await Dependencies.get_account(permissions.account_id) # policy = await Policy.find_one(Policy.policy_holder.id == account.id, fetch_links=True) # Temporarily workaround - policy_list = await PolicyActions.get_policies(resource=group, policy_holder=account) - policy = policy_list.policies[0] - policy = await Policy.get(policy.id, fetch_links=True) + policy_list = await PolicyActions.get_policies(resource=group, policy_holder=account) # type: ignore + policy = policy_list.policies[0] # type: ignore + policy = await Policy.get(policy.id, fetch_links=True) # type: ignore elif permissions.group_id: # Temporarily workaround group = await Dependencies.get_group(permissions.group_id) - policy_list = await PolicyActions.get_policies(resource=group, policy_holder=group) - policy = policy_list.policies[0] - policy = await Policy.get(policy.id, fetch_links=True) + policy_list = await PolicyActions.get_policies(resource=group, policy_holder=group) # type: ignore + policy = policy_list.policies[0] # type: ignore + policy = await Policy.get(policy.id, fetch_links=True) # type: ignore if not policy: raise APIException(404, "Policy not found 404") diff --git a/src/unipoll_api/routes/workspace.py b/src/unipoll_api/routes/workspace.py index 1d2bfd7..9f534d3 100644 --- a/src/unipoll_api/routes/workspace.py +++ b/src/unipoll_api/routes/workspace.py @@ -266,14 +266,14 @@ async def set_workspace_policy(workspace: Workspace = Depends(Dependencies.get_w # policy = await Policy.find_one(Policy.policy_holder.id == account.id, fetch_links=True) # Temporarily workaround policy_list = await actions.PolicyActions.get_policies(resource=workspace, policy_holder=account) - policy = policy_list.policies[0] - policy = await Policy.get(policy.id, fetch_links=True) + policy = policy_list.policies[0] # type: ignore + policy = await Policy.get(policy.id, fetch_links=True) # type: ignore elif permissions.group_id: # Temporarily workaround group = await Dependencies.get_group(permissions.group_id) policy_list = await actions.PolicyActions.get_policies(resource=workspace, policy_holder=group) - policy = policy_list.policies[0] - policy = await Policy.get(policy.id, fetch_links=True) + policy = policy_list.policies[0] # type: ignore + policy = await Policy.get(policy.id, fetch_links=True) # type: ignore if not policy: raise APIException(404, "Policy not found 404") From f74f84483e094d346f2fef9cd00b642e1fd1f023 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 11 Oct 2023 19:29:05 -0600 Subject: [PATCH 15/15] refactor: Added request schema for new group route Added GroupCreateRequest for new POST /groups endpoint that will replace workspace/{id}/groups --- src/unipoll_api/schemas/group.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/unipoll_api/schemas/group.py b/src/unipoll_api/schemas/group.py index ffed75d..bd57057 100644 --- a/src/unipoll_api/schemas/group.py +++ b/src/unipoll_api/schemas/group.py @@ -25,15 +25,27 @@ class GroupList(BaseModel): groups: list[GroupShort] | list[Group] +# Schema for the request to create a new group +class GroupCreateRequest(BaseModel): + name: str = Field(default="", min_length=3, max_length=50) + workspace: ResourceID = Field(title="Workspace ID") + description: str = Field(default="", title="Description", max_length=300) + model_config = ConfigDict(json_schema_extra={ + "example": { + "name": "Group 01", + "workspace": "60b9d1c8e1f1d5f5f5b4f8e1", + "description": "My first Group", + } + }) + + # Schema for the request to create a new group class GroupCreateInput(BaseModel): name: str = Field(default="", min_length=3, max_length=50) - workspace: ResourceID = Field(default=None, title="Workspace ID") description: str = Field(default="", title="Description", max_length=300) model_config = ConfigDict(json_schema_extra={ "example": { "name": "Group 01", - workspace: "60b9d1c8e1f1d5f5f5b4f8e1", "description": "My first Group", } })