Skip to content

Commit

Permalink
Merge branch 'main' of github.com:unipoll/API
Browse files Browse the repository at this point in the history
  • Loading branch information
mike-pisman committed Oct 23, 2023
2 parents 3e3692d + c6d3773 commit ff0de73
Show file tree
Hide file tree
Showing 19 changed files with 318 additions and 142 deletions.
74 changes: 74 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,80 @@



## v0.12.1 (2023-10-23)

### Fix

* fix: Changed policy_holder_type for new policies

The policy_holder_type was set to "member", updated the method to use type of policy_holder via get_document_type() method ([`6f595ff`](https://github.com/unipoll/api/commit/6f595ffb31be2b257ce477a91348499317e303e5))

### Refactor

* refactor: Changed argument type in ErrorWhileRemovingMember

Changed user: Account to member: Member ([`4301c36`](https://github.com/unipoll/api/commit/4301c36e258d206ff5ae94a2f69b87cc5636a4fa))

### Unknown

* Merge pull request #78 from unipoll/members

Patch Policies ([`a5100c0`](https://github.com/unipoll/api/commit/a5100c0b39265c63744b39fcb3e37f95c7ae9941))


## v0.12.0 (2023-10-23)

### Feature

* feat: Added Member document ([`8199204`](https://github.com/unipoll/api/commit/8199204906b16fe2ca4170b68c2ad004ad571d62))

### Fix

* fix: Removed PathOperations import ([`1cbfc93`](https://github.com/unipoll/api/commit/1cbfc9382cf38609a6a7572ca84db150cf7a0b0c))

* fix: Updated group request to get policies

Updated route get_group_policies to find member using dependency and use it for get_policies action ([`c16a981`](https://github.com/unipoll/api/commit/c16a9812a3f54be8f3f3387ad38bbd71d4b88098))

### Refactor

* refactor: Fixed mypy issues ([`d6ec5b8`](https://github.com/unipoll/api/commit/d6ec5b83b7a0e14f31b28995b94e13f4b9b4f785))

* refactor: Added get_document_type classmethod

Added classmethod to Beanie Document get_document_type which returns Document type as a string ([`cfeebf7`](https://github.com/unipoll/api/commit/cfeebf7efacac5e46699baa1948c625e9fd23804))

* refactor: Updated to accommodate member update ([`73e4017`](https://github.com/unipoll/api/commit/73e4017d3ce140bceba55ee43e779b657cd68fcd))

* refactor: Deleted obsolete path_operations file

This file included functions to read actions for building permissions and check permissions based on operation_id, both functions are not obsolete ([`095c103`](https://github.com/unipoll/api/commit/095c103f2aed2f9a66dfdcdf3de861d931185dc1))

### Style

* style: flake8 ([`b08c103`](https://github.com/unipoll/api/commit/b08c103344bbfc86c77544c2c4ba62f76d9a0124))

* style: Updated comments

Changed account to member ([`d29213a`](https://github.com/unipoll/api/commit/d29213a82dc473d081811e382982099cb4fb41a7))

### Test

* test: Updated tests to use new member document ([`8f7f352`](https://github.com/unipoll/api/commit/8f7f35244d09fe7159bf2f50c8980d32b1805e98))

* test: Updated workspace tests due to member update ([`af1dd25`](https://github.com/unipoll/api/commit/af1dd25c9d1d74a8d32e7158361a0d557dfdc949))

### Unknown

* Merge pull request #77 from unipoll/members

Updated Members ([`102afff`](https://github.com/unipoll/api/commit/102afff96b4df76433c4d497540f168084359040))

* Update README.md

Fixed link to developer wiki ([`a521dac`](https://github.com/unipoll/api/commit/a521dac3b95e70bc2a75c63eb5b961d89fb13df6))


## v0.11.3 (2023-10-17)

### Ci
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "unipoll-api"
version = "0.11.3"
version = "0.12.1"
description = "Unipoll API"
authors = [{email = "help@unipoll.cc"}, {name = "University Polling"}]
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion src/unipoll_api/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version = "0.11.3"
version = "0.12.1"
7 changes: 5 additions & 2 deletions src/unipoll_api/actions/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from unipoll_api.schemas import GroupSchemas, WorkspaceSchemas
from unipoll_api.exceptions import GroupExceptions, WorkspaceExceptions, ResourceExceptions
from unipoll_api.utils import Permissions
from unipoll_api.dependencies import get_member


# Get list of groups
Expand All @@ -22,7 +23,7 @@ async def get_groups(workspace: Workspace | None = None,
if workspace:
search_filter['workspace._id'] = workspace.id # type: ignore
if account:
search_filter['members._id'] = account.id # type: ignore
search_filter['members.account._id'] = account.id # type: ignore
search_result = await Group.find(search_filter, fetch_links=True).to_list()

# TODO: Rewrite to iterate over list of workspaces
Expand All @@ -47,6 +48,8 @@ async def create_group(workspace: Workspace,
await Permissions.check_permissions(workspace, "add_groups", check_permissions)
account = AccountManager.active_user.get()

member = await get_member(account, workspace)

# Check if group name is unique
group: Group # For type hinting, until Link type is supported
for group in workspace.groups: # type: ignore
Expand All @@ -63,7 +66,7 @@ async def create_group(workspace: Workspace,
raise GroupExceptions.ErrorWhileCreating(new_group)

# Add the account to group member list
await new_group.add_member(account, Permissions.GROUP_ALL_PERMISSIONS)
await new_group.add_member(member, Permissions.GROUP_ALL_PERMISSIONS)

# Create a policy for the new group
await workspace.add_policy(new_group, Permissions.WORKSPACE_BASIC_PERMISSIONS, False)
Expand Down
47 changes: 34 additions & 13 deletions src/unipoll_api/actions/members.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
from beanie import WriteRules
from beanie.operators import In
from unipoll_api.documents import Account, Group, ResourceID, Workspace
from unipoll_api.documents import Account, Group, ResourceID, Workspace, Member
from unipoll_api.utils import Permissions
from unipoll_api.schemas import MemberSchemas
# from unipoll_api import AccountManager
from unipoll_api.exceptions import ResourceExceptions
from unipoll_api.dependencies import get_member


async def get_members(resource: Workspace | Group, check_permissions: bool = True) -> MemberSchemas.MemberList:
# Check if the user has permission to add members
await Permissions.check_permissions(resource, "get_members", check_permissions)

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
def build_member_scheme(member: Member) -> MemberSchemas.Member:
account: Account = member.account # type: ignore
return MemberSchemas.Member(id=member.id,
account_id=account.id,
first_name=account.first_name,
last_name=account.last_name,
email=account.email)

member_list = [build_member_scheme(member) for member in resource.members] # type: ignore
# Return the list of members
Expand All @@ -36,29 +40,46 @@ async def add_members(resource: Workspace | Group,
account_list = await Account.find(In(Account.id, accounts)).to_list()
# Add the accounts to the group member list with basic permissions

new_members = []

for account in account_list:
default_permissions = eval("Permissions." + resource.resource_type.upper() + "_BASIC_PERMISSIONS")
await resource.add_member(account, default_permissions, save=False)
default_permissions = eval("Permissions." + resource.get_document_type().upper() + "_BASIC_PERMISSIONS")
if resource.get_document_type() == "Group":
member = await get_member(account, resource.workspace) # type: ignore
new_member = await resource.add_member(member, default_permissions, save=False)
new_members.append(new_member)
elif resource.get_document_type() == "Workspace":
new_member = await resource.add_member(account, default_permissions, save=False)
new_members.append(new_member)
await resource.save(link_rule=WriteRules.WRITE) # type: ignore

member_list = []
for new_member in new_members:
account: Account = new_member.account # type: ignore
member_list.append(MemberSchemas.Member(id=new_member.id,
account_id=account.id,
first_name=account.first_name,
last_name=account.last_name,
email=account.email))

# Return the list of members added to the group
return MemberSchemas.MemberList(members=[MemberSchemas.Member(**account.model_dump()) for account in account_list])
return MemberSchemas.MemberList(members=member_list)


# Remove a member from a workspace
async def remove_member(resource: Workspace | Group,
account: Account,
member: Member,
permission_check: bool = True) -> MemberSchemas.MemberList:
# Check if the user has permission to add members
await Permissions.check_permissions(resource, "remove_members", permission_check)

# 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)
if member.id not in [ResourceID(member.id) for member in resource.members]: # type: ignore
raise ResourceExceptions.ResourceNotFound("Member", member.id)

# Remove the account from the workspace/group
if await resource.remove_member(account):
if await resource.remove_member(member):
# 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)
raise ResourceExceptions.ErrorWhileRemovingMember(resource, member)
51 changes: 39 additions & 12 deletions src/unipoll_api/actions/policy.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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.documents import Account, Workspace, Group, Policy, Resource, Member
from unipoll_api.schemas import MemberSchemas, PolicySchemas, GroupSchemas
from unipoll_api.exceptions import ResourceExceptions
from unipoll_api.utils import Permissions
from unipoll_api.utils.permissions import check_permissions
from unipoll_api.dependencies import get_member


# Helper function to get policies from a resource
Expand All @@ -14,15 +15,17 @@ async def get_policies_from_resource(resource: Resource) -> list[Policy]:
await check_permissions(resource, "get_policies")
return resource.policies # type: ignore
except ResourceExceptions.UserNotAuthorized:
print("User not authorized")
account = AccountManager.active_user.get()
member = await get_member(account, resource)
for policy in resource.policies:
if policy.policy_holder.ref.id == account.id: # type: ignore
if policy.policy_holder.ref.id == member.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,
async def get_policies(policy_holder: Member | Group | None = None,
resource: Resource | None = None) -> PolicySchemas.PolicyList:
policy_list = []
policy: Policy
Expand Down Expand Up @@ -58,16 +61,28 @@ async def get_policy(policy: Policy, permission_check: bool = True) -> PolicySch

# Get the policy holder
policy_holder = await policy.get_policy_holder()
member = MemberSchemas.Member(**policy_holder.model_dump())
member, group = None, None
if policy_holder.get_document_type() == "Member":
await policy_holder.fetch_link("account")
account: Account = policy_holder.account # type: ignore
member = MemberSchemas.Member(id=policy_holder.id,
account_id=account.id,
email=account.email,
first_name=account.first_name,
last_name=account.last_name)
elif policy_holder.get_document_type() == "Group":
group = GroupSchemas.Group(id=policy_holder.id,
name=policy_holder.name,
description=policy_holder.description)

# Get the permissions based on the resource type and convert it to a list of strings
permission_type = Permissions.PermissionTypes[parent_resource.resource_type]
permission_type = Permissions.PermissionTypes[parent_resource.get_document_type()]
permissions = permission_type(policy.permissions).name.split('|') # type: ignore

# Return the policy
return PolicySchemas.PolicyShort(id=policy.id,
policy_holder_type=policy.policy_holder_type,
policy_holder=member.model_dump(exclude_unset=True),
policy_holder=member or group,
permissions=permissions)


Expand All @@ -79,7 +94,7 @@ async def update_policy(policy: Policy,

# Check if the user has the required permissions to update the policy
await Permissions.check_permissions(parent_resource, "update_policies", check_permissions)
permission_type = Permissions.PermissionTypes[parent_resource.resource_type]
permission_type = Permissions.PermissionTypes[parent_resource.get_document_type()]

# Calculate the new permission value from request
new_permission_value = 0
Expand All @@ -93,7 +108,19 @@ async def update_policy(policy: Policy,
await Policy.save(policy)

policy_holder = await policy.get_policy_holder()

return PolicySchemas.PolicyOutput(
permissions=permission_type(policy.permissions).name.split('|'), # type: ignore
policy_holder=policy_holder.model_dump())
member, group = None, None
if policy_holder.get_document_type() == "Member":
await policy_holder.fetch_link("account")
account: Account = policy_holder.account # type: ignore
member = MemberSchemas.Member(id=policy_holder.id,
account_id=account.id,
email=account.email,
first_name=account.first_name,
last_name=account.last_name)
elif policy_holder.get_document_type() == "Group":
group = GroupSchemas.Group(id=policy_holder.id,
name=policy_holder.name,
description=policy_holder.description)

return PolicySchemas.PolicyOutput(permissions=permission_type(policy.permissions).name.split('|'), # type: ignore
policy_holder=member or group)
18 changes: 10 additions & 8 deletions src/unipoll_api/actions/workspace.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
from bson import DBRef
from unipoll_api import AccountManager
from unipoll_api import actions
from unipoll_api.documents import Workspace, Account, Policy
from unipoll_api.documents import Workspace, Account, Policy, Member
from unipoll_api.utils import Permissions
from unipoll_api.schemas import WorkspaceSchemas
from unipoll_api.exceptions import WorkspaceExceptions
# from unipoll_api.dependencies import get_member


# Get a list of workspaces where the account is a owner/member
async def get_workspaces(account: Account | None = None) -> WorkspaceSchemas.WorkspaceList:
account = AccountManager.active_user.get()
account = AccountManager.active_user.get() if not account else account
workspace_list = []

search_result = await Workspace.find(Workspace.members.id == account.id).to_list() # type: ignore
members = await Member.find(Member.account.id == account.id, fetch_links=True).to_list()
workspaces = [member.workspace for member in members]

# Create a workspace list for output schema using the search results
for workspace in search_result:
for workspace in workspaces:
workspace_list.append(WorkspaceSchemas.WorkspaceShort(
**workspace.model_dump(exclude={'members', 'groups', 'permissions'})))
**workspace.model_dump(exclude={'groups', 'permissions'})))

return WorkspaceSchemas.WorkspaceList(workspaces=workspace_list)

Expand Down Expand Up @@ -71,22 +73,22 @@ async def update_workspace(workspace: Workspace,
await Permissions.check_permissions(workspace, "update_workspace", check_permissions)
save_changes = False

# Check if user suplied a name
# Check if user supplied a name
if input_data.name and input_data.name != workspace.name:
# Check if workspace name is unique
if await Workspace.find_one({"name": input_data.name}) and workspace.name != input_data.name:
raise WorkspaceExceptions.NonUniqueName(input_data.name)
workspace.name = input_data.name # Update the name
save_changes = True
# Check if user suplied a description
# Check if user supplied a description
if input_data.description and input_data.description != workspace.description:
workspace.description = input_data.description # Update the description
save_changes = True
# Save the updated workspace
if save_changes:
await Workspace.save(workspace)
# Return the updated workspace
return WorkspaceSchemas.Workspace(**workspace.model_dump())
return WorkspaceSchemas.Workspace(**workspace.model_dump(include={'id', 'name', 'description'}))


# Delete a workspace
Expand Down
14 changes: 13 additions & 1 deletion src/unipoll_api/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import Annotated
from functools import wraps
# from bson import DBRef
from fastapi import Cookie, Depends, Query, HTTPException, WebSocket
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.documents import ResourceID, Workspace, Group, Account, Poll, Policy, Member
from unipoll_api import exceptions as Exceptions


Expand All @@ -29,6 +30,17 @@ async def get_account(account_id: ResourceID) -> Account:
return account


async def get_member(account: Account, resource: Workspace | Group) -> Member:
"""
Returns a member with the given id.
"""

for member in resource.members:
if member.account.id == account.id: # type: ignore
return member # type: ignore
raise Exceptions.ResourceExceptions.ResourceNotFound("member", account.id)


async def websocket_auth(websocket: WebSocket,
session: Annotated[str | None, Cookie()] = None,
token: Annotated[str | None, Query()] = None) -> dict:
Expand Down
Loading

0 comments on commit ff0de73

Please sign in to comment.