From 77fe8503411fb6c930552bc5e33879423ffbb52b Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 29 Aug 2021 02:33:10 +0200 Subject: [PATCH 01/26] roles endpoint --- api/access_token.py | 27 +++ api/versions/v1/routers/roles/__init__.py | 4 + api/versions/v1/routers/roles/models.py | 27 +++ api/versions/v1/routers/roles/routes.py | 211 ++++++++++++++++++++++ api/versions/v1/routers/router.py | 3 + utils/__init__.py | 7 +- utils/permissions.py | 28 +++ 7 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 api/access_token.py create mode 100644 api/versions/v1/routers/roles/__init__.py create mode 100644 api/versions/v1/routers/roles/models.py create mode 100644 api/versions/v1/routers/roles/routes.py create mode 100644 utils/permissions.py diff --git a/api/access_token.py b/api/access_token.py new file mode 100644 index 0000000..d14dc06 --- /dev/null +++ b/api/access_token.py @@ -0,0 +1,27 @@ +import jwt +import config + +from typing import Optional +from fastapi import HTTPException, Request + + +async def access_token(request: Request) -> Optional[dict]: + """Attempts to locate and decode JWT token.""" + token = request.headers.get("authorization") + + if token is None: + raise HTTPException(status_code=401) + + try: + data = jwt.decode( + jwt=token, + algorithms=["HS256"], + key=config.secret_key(), + options=dict(verify_exp=True), + ) + except (jwt.PyJWTError, jwt.InvalidSignatureError): + raise HTTPException(status_code=403, detail="Invalid token.") + + data["uid"] = int(data["uid"]) + + return data diff --git a/api/versions/v1/routers/roles/__init__.py b/api/versions/v1/routers/roles/__init__.py new file mode 100644 index 0000000..9bd4168 --- /dev/null +++ b/api/versions/v1/routers/roles/__init__.py @@ -0,0 +1,4 @@ +from .routes import router + + +__all__ = (router,) diff --git a/api/versions/v1/routers/roles/models.py b/api/versions/v1/routers/roles/models.py new file mode 100644 index 0000000..742fd03 --- /dev/null +++ b/api/versions/v1/routers/roles/models.py @@ -0,0 +1,27 @@ +from typing import List, Optional +from pydantic import BaseModel, Field + + +class RoleResponse(BaseModel): + id: str + name: str + position: int + permissions: int + color: Optional[int] + + +class DetailedRoleResponse(RoleResponse): + members: List[str] + + +class NewRoleBody(BaseModel): + name: str = Field(object, min_length=4, max_length=64) + color: int = Field(None, le=0xFFFFFF, ge=0) + permissions: int = Field(0) + + +class UpdateRoleBody(BaseModel): + name: str = Field(object, min_length=4, max_length=64) + color: int = Field(object, le=0xFFFFFF, ge=0) + permissions: int = Field(object) + position: int = Field(object) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py new file mode 100644 index 0000000..16ddfe1 --- /dev/null +++ b/api/versions/v1/routers/roles/routes.py @@ -0,0 +1,211 @@ +import utils + +from typing import List +from fastapi import APIRouter, Depends, HTTPException, Query + +from api.models import Role +from api.access_token import access_token +from api.models.permissions import ManageRoles +from api.versions.v1.routers.roles.models import ( + NewRoleBody, + RoleResponse, + UpdateRoleBody, + DetailedRoleResponse, +) + + +router = APIRouter(prefix="/roles") + + +@router.get("", tags=["roles"], response_model=List[RoleResponse]) +async def fetch_all_roles( + limit: int = Query(None), + offset: int = Query(None), +): + query = """ + SELECT + *, id::TEXT + FROM roles + LIMIT $1 + OFFSET $2; + """ + records = await Role.pool.fetch(query, limit, offset) + + return [dict(record) for record in records] + + +@router.get("/{id}", tags=["roles"], response_model=DetailedRoleResponse) +async def fetch_role(id: int): + query = """ + SELECT + *, + id::TEXT, + COALESCE( + ( + SELECT + json_agg(user_id::TEXT) + FROM userroles ur + WHERE ur.role_id = $1 + ), '[]' + ) members + FROM roles r + WHERE r.id = $1; + """ + record = await Role.pool.fetchrow(query, id) + + return dict(record) + + +@router.post("", tags=["roles"], response_model=RoleResponse) +async def create_role(body: NewRoleBody, token: str = Depends(access_token)): + query = """ + WITH user_roles AS ( + SELECT role_id FROM userroles WHERE user_id = $1 + ) + SELECT permissions FROM roles WHERE id IN (SELECT * FROM user_roles); + """ + + records = await Role.pool.fetch(query, token["uid"]) + if not records: + raise HTTPException(403, "Missing Permissions") + + user_permissions = 0 + for record in records: + user_permissions |= record["permissions"] + + if not utils.has_permission( + user_permissions, body.permissions + ) or not utils.has_permission(user_permissions, ManageRoles()): + raise HTTPException(403, "Missing Permissions") + + query = """ + INSERT INTO roles (id, name, color, permissions, position) + VALUES (create_snowflake(), $1, $2, $3, (SELECT COUNT(*) FROM roles) + 1) + RETURNING *; + """ + record = await Role.pool.fetchrow(query, body.name, body.color, body.permissions) + + return dict(record) + + +@router.patch("/{id}", tags=["roles"]) +async def update_role( + id: int, body: UpdateRoleBody, token: str = Depends(access_token) +): + role = await Role.fetch(id) + if not role: + raise HTTPException(404, "Role Not Found") + + query = """ + WITH user_roles AS ( + SELECT role_id FROM userroles WHERE user_id = $1 + ) + SELECT position, permissions FROM roles WHERE id IN (SELECT * FROM user_roles); + """ + + records = await Role.pool.fetch(query, token["uid"]) + if not records: + raise HTTPException(403, "Missing Permissions") + + user_permissions = 0 + for record in records: + user_permissions |= record["permissions"] + + top_role = max(records, key=lambda record: record["position"]) + if ( + not utils.has_permission(user_permissions, ManageRoles()) + or top_role["position"] >= role.position + ): + raise HTTPException(403, "Missing Permissions") + + data = body.dict() + if not utils.has_permission(user_permissions, data["permissions"]): + raise HTTPException(403, "Missing Permissions") + + if (position := data.pop("position")) != object and position != role.position: + if position <= top_role["position"]: + raise HTTPException(403, "Missing Permissions") + + if position > role.position: + new_pos = position + 0.5 + else: + new_pos = position - 0.5 + + query = """ + UPDATE roles r SET position = $1 + WHERE r.id = $2; + """ + await Role.pool.execute(query, new_pos, id) + + query = """ + WITH todo AS ( + SELECT r.id, + ROW_NUMBER() OVER (ORDER BY position) AS position + FROM roles r + ) + UPDATE roles r SET + position = td.position + FROM todo td + WHERE r.id = td.id; + """ + await Role.pool.execute(query) + + new_data = list(filter(lambda key: data[key] != object, data)) + if new_data: + query = "UPDATE ROLES SET " + query += ", ".join("%s = %d" % (key, i) for i, key in enumerate(new_data, 2)) + query += " WHERE id = $1" + + await Role.pool.execute(query, id, *data.values()) + + return utils.JSONResponse(status_code=204) + + +@router.delete("/{id}", tags=["roles"]) +async def delete_role(id: int, token=Depends(access_token)): + role = await Role.fetch(id) + if not role: + raise HTTPException(404, "Role Not Found") + + query = """ + WITH user_roles AS ( + SELECT role_id FROM userroles WHERE user_id = $1 + ) + SELECT position, permissions FROM roles WHERE id IN (SELECT * FROM user_roles); + """ + + records = await Role.pool.fetch(query, token["uid"]) + if not records: + raise HTTPException(403, "Missing Permissions") + + user_permissions = 0 + for record in records: + user_permissions |= record["permissions"] + + top_role = max(records, key=lambda record: record["position"]) + if ( + not utils.has_permission(user_permissions, ManageRoles()) + or top_role["position"] >= role.position + ): + raise HTTPException(403, "Missing Permissions") + + query = """ + WITH deleted AS ( + DELETE FROM roles r + WHERE r.id = $1 + RETURNING r.position + ), + to_update AS ( + SELECT r.id + FROM roles r + WHERE r.position > (SELECT position FROM deleted) + ), + updated AS ( + UPDATE roles r SET position = r.position - 1 + WHERE r.id IN (SELECT id FROM to_update) + ) + SELECT 1; + """ + await Role.pool.execute(query, id) + + return utils.JSONResponse(status_code=204) diff --git a/api/versions/v1/routers/router.py b/api/versions/v1/routers/router.py index f9810cc..5fe0698 100644 --- a/api/versions/v1/routers/router.py +++ b/api/versions/v1/routers/router.py @@ -1,6 +1,9 @@ from fastapi import APIRouter + from . import auth +from . import roles router = APIRouter(prefix="/v1") router.include_router(auth.router) +router.include_router(roles.router) diff --git a/utils/__init__.py b/utils/__init__.py index 7670ed7..7723b5a 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1,7 +1,10 @@ from .time import snowflake_time from .response import JSONResponse +from .permissions import has_permission, has_permissions __all__ = ( - "JSONResponse", - "snowflake_time", + JSONResponse, + snowflake_time, + has_permission, + has_permissions, ) diff --git a/utils/permissions.py b/utils/permissions.py new file mode 100644 index 0000000..cc15f79 --- /dev/null +++ b/utils/permissions.py @@ -0,0 +1,28 @@ +from typing import Union, List +from api.models.permissions import BasePermission, Administrator + + +def has_permissions( + permissions: int, required: List[Union[int, BasePermission]] +) -> bool: + if permissions & Administrator().value: + return True + + all_perms = 0 + for perm in required: + if isinstance(perm, int): + all_perms |= perm + else: + all_perms |= perm.value + + return permissions & all_perms == all_perms + + +def has_permission(permissions: int, permission: Union[BasePermission, int]) -> bool: + if permissions & Administrator().value: + return True + + if isinstance(permission, int): + return permissions & permission == permission + + return permissions & permission.value == permission.value From 6b2b83e9c87327347604b6a47dd0e2fd0645745c Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 29 Aug 2021 10:53:40 +0200 Subject: [PATCH 02/26] fix delete query --- api/versions/v1/routers/roles/routes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index 16ddfe1..7dbd5ee 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -193,18 +193,18 @@ async def delete_role(id: int, token=Depends(access_token)): WITH deleted AS ( DELETE FROM roles r WHERE r.id = $1 - RETURNING r.position + RETURNING r.id ), to_update AS ( - SELECT r.id - FROM roles r - WHERE r.position > (SELECT position FROM deleted) - ), - updated AS ( - UPDATE roles r SET position = r.position - 1 - WHERE r.id IN (SELECT id FROM to_update) + SELECT r.id, + ROW_NUMBER() OVER (ORDER BY r.position) AS position + FROM roles r + WHERE r.id != (SELECT id FROM deleted) ) - SELECT 1; + UPDATE roles r SET + position = tu.position + FROM to_update tu + WHERE r.id = tu.id """ await Role.pool.execute(query, id) From 2b9b2a8a8ebceb615745183304aa96b9800b8909 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 29 Aug 2021 13:42:00 +0200 Subject: [PATCH 03/26] fix docs --- api/versions/v1/routers/roles/models.py | 14 +++++++------- api/versions/v1/routers/roles/routes.py | 15 +++++++-------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/api/versions/v1/routers/roles/models.py b/api/versions/v1/routers/roles/models.py index 742fd03..4b08d29 100644 --- a/api/versions/v1/routers/roles/models.py +++ b/api/versions/v1/routers/roles/models.py @@ -15,13 +15,13 @@ class DetailedRoleResponse(RoleResponse): class NewRoleBody(BaseModel): - name: str = Field(object, min_length=4, max_length=64) - color: int = Field(None, le=0xFFFFFF, ge=0) - permissions: int = Field(0) + name: str = Field(..., min_length=4, max_length=64) + color: Optional[int] = Field(None, le=0xFFFFFF, ge=0) + permissions: Optional[int] = Field(0, ge=0) class UpdateRoleBody(BaseModel): - name: str = Field(object, min_length=4, max_length=64) - color: int = Field(object, le=0xFFFFFF, ge=0) - permissions: int = Field(object) - position: int = Field(object) + name: str = Field("", min_length=4, max_length=64) + color: Optional[int] = Field(None, le=0xFFFFFF, ge=0) + permissions: int = Field(0, ge=0) + position: int = Field(0, ge=0) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index 7dbd5ee..5dd3f71 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -89,9 +89,7 @@ async def create_role(body: NewRoleBody, token: str = Depends(access_token)): @router.patch("/{id}", tags=["roles"]) -async def update_role( - id: int, body: UpdateRoleBody, token: str = Depends(access_token) -): +async def update_role(id: int, body: UpdateRoleBody, token=Depends(access_token)): role = await Role.fetch(id) if not role: raise HTTPException(404, "Role Not Found") @@ -118,11 +116,13 @@ async def update_role( ): raise HTTPException(403, "Missing Permissions") - data = body.dict() + data = body.dict(exclude_unset=True) if not utils.has_permission(user_permissions, data["permissions"]): raise HTTPException(403, "Missing Permissions") - if (position := data.pop("position")) != object and position != role.position: + if ( + position := data.pop("position", None) + ) is not None and position != role.position: if position <= top_role["position"]: raise HTTPException(403, "Missing Permissions") @@ -150,10 +150,9 @@ async def update_role( """ await Role.pool.execute(query) - new_data = list(filter(lambda key: data[key] != object, data)) - if new_data: + if data: query = "UPDATE ROLES SET " - query += ", ".join("%s = %d" % (key, i) for i, key in enumerate(new_data, 2)) + query += ", ".join("%s = %d" % (key, i) for i, key in enumerate(data, 2)) query += " WHERE id = $1" await Role.pool.execute(query, id, *data.values()) From a8c09b2ab8b81e4edbb45e2cd791518b90daa387 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 29 Aug 2021 13:42:41 +0200 Subject: [PATCH 04/26] update submodules --- api/models | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/models b/api/models index 50531e1..1fa3de8 160000 --- a/api/models +++ b/api/models @@ -1 +1 @@ -Subproject commit 50531e15ab5eac0db6c1d15469a396932f6f1b37 +Subproject commit 1fa3de80ff133cd3b5be5e2d000cbe0f84d79c09 From 7521bc2ded3d0d6b2242c04e386d0cc7ab01ed55 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 29 Aug 2021 14:21:30 +0200 Subject: [PATCH 05/26] fix permissions check --- api/versions/v1/routers/roles/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index 5dd3f71..d6240d7 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -117,7 +117,7 @@ async def update_role(id: int, body: UpdateRoleBody, token=Depends(access_token) raise HTTPException(403, "Missing Permissions") data = body.dict(exclude_unset=True) - if not utils.has_permission(user_permissions, data["permissions"]): + if not utils.has_permission(user_permissions, body.permissions): raise HTTPException(403, "Missing Permissions") if ( From 495c294d59ec218bfe027136f382f45d738a6977 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sat, 4 Sep 2021 15:42:42 +0200 Subject: [PATCH 06/26] role members endpoints --- api/access_token.py | 27 -------- api/dependencies.py | 52 +++++++++++++++ api/versions/v1/routers/roles/routes.py | 88 +++++++++++++++++++++++-- 3 files changed, 133 insertions(+), 34 deletions(-) delete mode 100644 api/access_token.py create mode 100644 api/dependencies.py diff --git a/api/access_token.py b/api/access_token.py deleted file mode 100644 index d14dc06..0000000 --- a/api/access_token.py +++ /dev/null @@ -1,27 +0,0 @@ -import jwt -import config - -from typing import Optional -from fastapi import HTTPException, Request - - -async def access_token(request: Request) -> Optional[dict]: - """Attempts to locate and decode JWT token.""" - token = request.headers.get("authorization") - - if token is None: - raise HTTPException(status_code=401) - - try: - data = jwt.decode( - jwt=token, - algorithms=["HS256"], - key=config.secret_key(), - options=dict(verify_exp=True), - ) - except (jwt.PyJWTError, jwt.InvalidSignatureError): - raise HTTPException(status_code=403, detail="Invalid token.") - - data["uid"] = int(data["uid"]) - - return data diff --git a/api/dependencies.py b/api/dependencies.py new file mode 100644 index 0000000..629750b --- /dev/null +++ b/api/dependencies.py @@ -0,0 +1,52 @@ +import jwt +import utils +import config + +from typing import List, Optional, Union +from fastapi import Depends, HTTPException, Request + +from api.models import Role +from api.models.permissions import BasePermission + + +async def access_token(request: Request) -> Optional[dict]: + """Attempts to locate and decode JWT token.""" + token = request.headers.get("authorization") + + if token is None: + raise HTTPException(status_code=401) + + try: + data = jwt.decode( + jwt=token, + algorithms=["HS256"], + key=config.secret_key(), + ) + except (jwt.PyJWTError, jwt.InvalidSignatureError): + raise HTTPException(status_code=401, detail="Invalid token.") + + data["uid"] = int(data["uid"]) + + return data + + +async def has_permissions(permissions: List[Union[int, BasePermission]]): + async def inner(token=Depends(access_token)): + query = """ + WITH user_roles AS ( + SELECT role_id FROM userroles WHERE user_id = $1 + ) + SELECT position, permissions FROM roles WHERE id IN (SELECT * FROM user_roles); + """ + + records = await Role.pool.fetch(query, token["uid"]) + if not records: + raise HTTPException(403, "Missing Permissions") + + user_permissions = 0 + for record in records: + user_permissions |= record["permissions"] + + return utils.has_permissions(user_permissions, permissions) + + return Depends(inner) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index d6240d7..440865d 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -1,10 +1,11 @@ import utils +import asyncpg -from typing import List -from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Union +from fastapi import APIRouter, Depends, HTTPException, Query, Response -from api.models import Role -from api.access_token import access_token +from api.models import Role, UserRole +from api.dependencies import access_token from api.models.permissions import ManageRoles from api.versions.v1.routers.roles.models import ( NewRoleBody, @@ -57,7 +58,7 @@ async def fetch_role(id: int): @router.post("", tags=["roles"], response_model=RoleResponse) -async def create_role(body: NewRoleBody, token: str = Depends(access_token)): +async def create_role(body: NewRoleBody, token=Depends(access_token)): query = """ WITH user_roles AS ( SELECT role_id FROM userroles WHERE user_id = $1 @@ -109,7 +110,7 @@ async def update_role(id: int, body: UpdateRoleBody, token=Depends(access_token) for record in records: user_permissions |= record["permissions"] - top_role = max(records, key=lambda record: record["position"]) + top_role = min(records, key=lambda record: record["position"]) if ( not utils.has_permission(user_permissions, ManageRoles()) or top_role["position"] >= role.position @@ -181,7 +182,7 @@ async def delete_role(id: int, token=Depends(access_token)): for record in records: user_permissions |= record["permissions"] - top_role = max(records, key=lambda record: record["position"]) + top_role = min(records, key=lambda record: record["position"]) if ( not utils.has_permission(user_permissions, ManageRoles()) or top_role["position"] >= role.position @@ -208,3 +209,76 @@ async def delete_role(id: int, token=Depends(access_token)): await Role.pool.execute(query, id) return utils.JSONResponse(status_code=204) + + +@router.put("/{role_id}/{member_id}", tags=["roles"]) +async def add_member_to_role( + role_id: int, member_id: int, token=Depends(access_token) +) -> Union[Response, utils.JSONResponse]: + role = await Role.fetch(role_id) + if not role: + raise HTTPException(404, "Role Not Found") + + query = """ + WITH user_roles AS ( + SELECT role_id FROM userroles WHERE user_id = $1 + ) + SELECT position, permissions FROM roles WHERE id IN (SELECT * FROM user_roles); + """ + + records = await Role.pool.fetch(query, token["uid"]) + if not records: + raise HTTPException(403, "Missing Permissions") + + user_permissions = 0 + for record in records: + user_permissions |= record["permissions"] + + top_role = min(records, key=lambda record: record["position"]) + if ( + not utils.has_permission(user_permissions, ManageRoles()) + or top_role["position"] >= role.position + ): + raise HTTPException(403, "Missing Permissions") + + try: + await UserRole.create(member_id, role_id) + except asyncpg.exceptions.UniqueViolationError: + raise HTTPException(409, "User already has the role") + + return Response(status_code=204, content="") + + +@router.delete("/{role_id}/{member_id}", tags=["roles"]) +async def remove_member_from_role( + role_id: int, member_id: int, token=Depends(access_token) +) -> Union[Response, utils.JSONResponse]: + role = await Role.fetch(role_id) + if not role: + raise HTTPException(404, "Role Not Found") + + query = """ + WITH user_roles AS ( + SELECT role_id FROM userroles WHERE user_id = $1 + ) + SELECT position, permissions FROM roles WHERE id IN (SELECT * FROM user_roles); + """ + + records = await Role.pool.fetch(query, token["uid"]) + if not records: + raise HTTPException(403, "Missing Permissions") + + user_permissions = 0 + for record in records: + user_permissions |= record["permissions"] + + top_role = min(records, key=lambda record: record["position"]) + if ( + not utils.has_permission(user_permissions, ManageRoles()) + or top_role["position"] >= role.position + ): + raise HTTPException(403, "Missing Permissions") + + await UserRole.delete(member_id, role_id) + + return Response(status_code=204, content="") From 6756a130cdac7eadc380b0edaeedc9e5ef968e60 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sat, 4 Sep 2021 15:46:37 +0200 Subject: [PATCH 07/26] update docs --- api/app.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/app.py b/api/app.py index da496f0..1f5c3cb 100644 --- a/api/app.py +++ b/api/app.py @@ -10,7 +10,12 @@ log = logging.getLogger() -app = FastAPI() +app = FastAPI( + title="Tech With Tim", + docs_url="/api/docs", + redoc_url="/api/redoc", + openapi_url="/api/docs/openapi.json", +) app.router.prefix = "/api" app.router.default_response_class = JSONResponse From c8aa51b8667435301e170e313410d594d79f7821 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sat, 4 Sep 2021 17:17:31 +0200 Subject: [PATCH 08/26] update status code --- api/versions/v1/routers/roles/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index 440865d..d3cd5a1 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -86,7 +86,7 @@ async def create_role(body: NewRoleBody, token=Depends(access_token)): """ record = await Role.pool.fetchrow(query, body.name, body.color, body.permissions) - return dict(record) + return utils.JSONResponse(status_code=201, content=dict(record)) @router.patch("/{id}", tags=["roles"]) From 1ffc15c4b3e3d615ddf70712d3ffd21a4588a72f Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 15:51:13 +0200 Subject: [PATCH 09/26] fixes --- api/models | 2 +- api/versions/v1/routers/roles/models.py | 2 +- api/versions/v1/routers/roles/routes.py | 19 +++++++++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/api/models b/api/models index 1fa3de8..f11c37d 160000 --- a/api/models +++ b/api/models @@ -1 +1 @@ -Subproject commit 1fa3de80ff133cd3b5be5e2d000cbe0f84d79c09 +Subproject commit f11c37d62259f36731d0981a8fb99b3d78186550 diff --git a/api/versions/v1/routers/roles/models.py b/api/versions/v1/routers/roles/models.py index 4b08d29..712cb89 100644 --- a/api/versions/v1/routers/roles/models.py +++ b/api/versions/v1/routers/roles/models.py @@ -15,7 +15,7 @@ class DetailedRoleResponse(RoleResponse): class NewRoleBody(BaseModel): - name: str = Field(..., min_length=4, max_length=64) + name: str = Field(..., min_length=4, max_length=32) color: Optional[int] = Field(None, le=0xFFFFFF, ge=0) permissions: Optional[int] = Field(0, ge=0) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index d3cd5a1..da9fb10 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -84,7 +84,12 @@ async def create_role(body: NewRoleBody, token=Depends(access_token)): VALUES (create_snowflake(), $1, $2, $3, (SELECT COUNT(*) FROM roles) + 1) RETURNING *; """ - record = await Role.pool.fetchrow(query, body.name, body.color, body.permissions) + try: + record = await Role.pool.fetchrow( + query, body.name, body.color, body.permissions + ) + except asyncpg.exceptions.UniqueViolationError: + raise HTTPException(409, "Role with that name already exists") return utils.JSONResponse(status_code=201, content=dict(record)) @@ -121,6 +126,12 @@ async def update_role(id: int, body: UpdateRoleBody, token=Depends(access_token) if not utils.has_permission(user_permissions, body.permissions): raise HTTPException(403, "Missing Permissions") + if name := data.get("name", None): + record = await Role.pool.fetchrow("SELECT * FROM roles WHERE name = $1", name) + + if record: + raise HTTPException(409, "Role with that name already exists") + if ( position := data.pop("position", None) ) is not None and position != role.position: @@ -153,7 +164,7 @@ async def update_role(id: int, body: UpdateRoleBody, token=Depends(access_token) if data: query = "UPDATE ROLES SET " - query += ", ".join("%s = %d" % (key, i) for i, key in enumerate(data, 2)) + query += ", ".join("%s = $%d" % (key, i) for i, key in enumerate(data, 2)) query += " WHERE id = $1" await Role.pool.execute(query, id, *data.values()) @@ -211,7 +222,7 @@ async def delete_role(id: int, token=Depends(access_token)): return utils.JSONResponse(status_code=204) -@router.put("/{role_id}/{member_id}", tags=["roles"]) +@router.put("/{role_id}/members/{member_id}", tags=["roles"]) async def add_member_to_role( role_id: int, member_id: int, token=Depends(access_token) ) -> Union[Response, utils.JSONResponse]: @@ -249,7 +260,7 @@ async def add_member_to_role( return Response(status_code=204, content="") -@router.delete("/{role_id}/{member_id}", tags=["roles"]) +@router.delete("/{role_id}/members/{member_id}", tags=["roles"]) async def remove_member_from_role( role_id: int, member_id: int, token=Depends(access_token) ) -> Union[Response, utils.JSONResponse]: From 6450261dc846e3e5855f157fede040714bd4831f Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 15:58:33 +0200 Subject: [PATCH 10/26] tests --- tests/conftest.py | 25 ++- tests/test_auth.py | 10 +- tests/test_roles.py | 393 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 420 insertions(+), 8 deletions(-) create mode 100644 tests/test_roles.py diff --git a/tests/conftest.py b/tests/conftest.py index c5f93ab..4cc602e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,13 @@ -from launch import prepare_postgres, safe_create_tables, delete_tables +import jwt import config +import pytest +import asyncio -from httpx import AsyncClient from postDB import Model -import asyncio -import pytest +from httpx import AsyncClient + +from api.models import User +from launch import prepare_postgres, safe_create_tables, delete_tables @pytest.fixture(scope="session") @@ -34,6 +37,20 @@ async def db(event_loop) -> bool: await delete_tables() +@pytest.fixture +async def user(db): + yield await User.create(0, "Test", "0001") + await db.execute("""DELETE FROM users WHERE username = 'Test'""") + + +@pytest.fixture +async def token(user, db): + yield jwt.encode( + {"uid": user.id}, + key=config.secret_key(), + ) + + def pytest_addoption(parser): parser.addoption( "--no-db", diff --git a/tests/test_auth.py b/tests/test_auth.py index e1eee88..55e46b2 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -84,10 +84,10 @@ async def exchange_code(**kwargs): async def get_user(**kwargs): return { - "username": "M7MD", - "discriminator": "1701", - "id": 601173582516584602, - "avatar": "135fa48ba8f26417c4b9818ae2e37aa0", + "id": 1, + "username": "Test2", + "avatar": "avatar", + "discriminator": "0001", } mocker.patch("api.versions.v1.routers.auth.routes.get_user", new=get_user) @@ -99,3 +99,5 @@ async def get_user(**kwargs): ) assert res.status_code == 200 + + await db.execute("DELETE FROM users WHERE id = 1") diff --git a/tests/test_roles.py b/tests/test_roles.py new file mode 100644 index 0000000..7284b2e --- /dev/null +++ b/tests/test_roles.py @@ -0,0 +1,393 @@ +import pytest + +from httpx import AsyncClient + +from api.models import Role, UserRole +from api.models.permissions import ManageRoles + + +@pytest.fixture +async def manage_roles_role(db): + query = """ + INSERT INTO roles (id, name, color, permissions, position) + VALUES (create_snowflake(), $1, $2, $3, (SELECT COUNT(*) FROM roles) + 1) + RETURNING *; + """ + record = await Role.pool.fetchrow(query, "Roles Manager", 0x0, ManageRoles().value) + yield Role(**record) + await db.execute("DELETE FROM roles WHERE id = $1;", record["id"]) + + +@pytest.mark.db +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("data", "status"), + [ + ({}, 422), + ({"name": ""}, 422), + ({"permissions": -1}, 422), + ({"name": "test1", "color": 0xFFFFFFF}, 422), + ({"name": "test1", "color": -0x000001}, 422), + ({"name": "test2", "color": 0x000000, "permissions": 8}, 403), + ({"name": "test2", "color": 0x000000, "permissions": 0}, 201), + ({"name": "test2", "color": 0x000000, "permissions": 0}, 409), + ], +) +async def test_role_create( + app: AsyncClient, db, user, token, manage_roles_role, data, status +): + try: + await UserRole.create(user.id, manage_roles_role.id) + res = await app.post( + "/api/v1/roles", json=data, headers={"Authorization": token} + ) + assert res.status_code == status + finally: + await db.execute( + "DELETE FROM userroles WHERE role_id = $1 AND user_id = $2;", + manage_roles_role.id, + user.id, + ) + if status == 409: + await db.execute("DELETE FROM roles WHERE name = $1", data["name"]) + + +@pytest.mark.db +@pytest.mark.asyncio +async def test_fetch_all_roles(app: AsyncClient): + res = await app.get("/api/v1/roles") + + assert res.status_code == 200 + assert type(res.json()) == list + + +@pytest.mark.db +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("request_data", "new_data", "status"), + [ + ({}, {"name": "test update", "permissions": 0, "color": 0}, 204), + ({"name": ""}, {"name": "test update", "permissions": 0, "color": 0}, 422), + ( + {"permissions": -1}, + {"name": "test update", "permissions": 0, "color": 0}, + 422, + ), + ( + {"color": 0xFFFFFFF}, + {"name": "test update", "permissions": 0, "color": 0}, + 422, + ), + ( + {"color": -0x000001}, + {"name": "test update", "permissions": 0, "color": 0}, + 422, + ), + ( + {"color": 0x5, "permissions": 8}, + {"name": "test update", "permissions": 0, "color": 0x0}, + 403, + ), + ( + {"color": 0x5, "permissions": ManageRoles().value}, + {"name": "test update", "permissions": ManageRoles().value, "color": 0x5}, + 204, + ), + ], +) +async def test_role_update( + app: AsyncClient, db, user, token, manage_roles_role, request_data, new_data, status +): + try: + query = """ + INSERT INTO roles (id, name, color, permissions, position) + VALUES (create_snowflake(), 'test update', 0, 0, (SELECT COUNT(*) FROM roles) + 1) + RETURNING *; + """ + role = Role(**await Role.pool.fetchrow(query)) + await UserRole.create(user.id, manage_roles_role.id) + + res = await app.patch( + f"/api/v1/roles/{role.id}", + json=request_data, + headers={"Authorization": token}, + ) + + assert res.status_code == status + + role = await Role.fetch(role.id) + + data = role.as_dict() + data.pop("id") + data.pop("position") + + assert data == new_data + finally: + await db.execute( + "DELETE FROM userroles WHERE role_id = $1 AND user_id = $2;", + manage_roles_role.id, + user.id, + ) + await db.execute("DELETE FROM roles WHERE id = $1", role.id) + + +@pytest.mark.db +@pytest.mark.asyncio +async def test_role_delete(app: AsyncClient, db, user, token, manage_roles_role): + try: + query = """ + INSERT INTO roles (id, name, color, permissions, position) + VALUES (create_snowflake(), 'test delete', 0, 0, (SELECT COUNT(*) FROM roles) + 1) + RETURNING *; + """ + role = Role(**await Role.pool.fetchrow(query)) + await UserRole.create(user.id, manage_roles_role.id) + + res = await app.delete( + f"/api/v1/roles/{role.id}", + headers={"Authorization": token}, + ) + + assert res.status_code == 204 + finally: + await db.execute( + "DELETE FROM userroles WHERE role_id = $1 AND user_id = $2;", + manage_roles_role.id, + user.id, + ) + await db.execute("DELETE FROM roles WHERE id = $1", role.id) + + +@pytest.mark.db +@pytest.mark.asyncio +async def test_role_delete_high_position( + app: AsyncClient, db, user, token, manage_roles_role +): + try: + query = """ + INSERT INTO roles (id, name, color, permissions, position) + VALUES (create_snowflake(), 'test delete', 0, 0, 0) + RETURNING *; + """ + role = Role(**await Role.pool.fetchrow(query)) + await UserRole.create(user.id, manage_roles_role.id) + + res = await app.delete( + f"/api/v1/roles/{role.id}", + headers={"Authorization": token}, + ) + + assert res.status_code == 403 + finally: + await db.execute( + "DELETE FROM userroles WHERE role_id = $1 AND user_id = $2;", + manage_roles_role.id, + user.id, + ) + await db.execute("DELETE FROM roles WHERE id = $1", role.id) + + +@pytest.mark.db +@pytest.mark.asyncio +async def test_role_add(app: AsyncClient, db, user, token, manage_roles_role): + try: + query = """ + INSERT INTO roles (id, name, color, permissions, position) + VALUES (create_snowflake(), 'test add', 0, 0, (SELECT COUNT(*) FROM roles) + 1) + RETURNING *; + """ + role = Role(**await Role.pool.fetchrow(query)) + await UserRole.create(user.id, manage_roles_role.id) + + res = await app.put( + f"/api/v1/roles/{role.id}/members/{user.id}", + headers={"Authorization": token}, + ) + + assert res.status_code == 204 + finally: + await db.execute( + "DELETE FROM userroles WHERE role_id = $1 AND user_id = $2;", + manage_roles_role.id, + user.id, + ) + await db.execute("DELETE FROM roles WHERE id = $1", role.id) + + +@pytest.mark.db +@pytest.mark.asyncio +async def test_role_add_high_position( + app: AsyncClient, db, user, token, manage_roles_role +): + try: + query = """ + INSERT INTO roles (id, name, color, permissions, position) + VALUES (create_snowflake(), 'test add', 0, 0, 0) + RETURNING *; + """ + role = Role(**await Role.pool.fetchrow(query)) + await UserRole.create(user.id, manage_roles_role.id) + + res = await app.put( + f"/api/v1/roles/{role.id}/members/{user.id}", + headers={"Authorization": token}, + ) + + assert res.status_code == 403 + finally: + await db.execute( + "DELETE FROM userroles WHERE role_id = $1 AND user_id = $2;", + manage_roles_role.id, + user.id, + ) + await db.execute("DELETE FROM roles WHERE id = $1", role.id) + + +@pytest.mark.db +@pytest.mark.asyncio +async def test_role_remove(app: AsyncClient, db, user, token, manage_roles_role): + try: + query = """ + INSERT INTO roles (id, name, color, permissions, position) + VALUES (create_snowflake(), 'test remove', 0, 0, (SELECT COUNT(*) FROM roles) + 1) + RETURNING *; + """ + role = Role(**await Role.pool.fetchrow(query)) + await UserRole.create(user.id, manage_roles_role.id) + + res = await app.delete( + f"/api/v1/roles/{role.id}/members/{user.id}", + headers={"Authorization": token}, + ) + + assert res.status_code == 204 + finally: + await db.execute( + "DELETE FROM userroles WHERE role_id = $1 AND user_id = $2;", + manage_roles_role.id, + user.id, + ) + await db.execute("DELETE FROM roles WHERE id = $1", role.id) + + +@pytest.mark.db +@pytest.mark.asyncio +async def test_role_remove_high_position( + app: AsyncClient, db, user, token, manage_roles_role +): + try: + query = """ + INSERT INTO roles (id, name, color, permissions, position) + VALUES (create_snowflake(), 'test remove', 0, 0, 0) + RETURNING *; + """ + role = Role(**await Role.pool.fetchrow(query)) + await UserRole.create(user.id, manage_roles_role.id) + + res = await app.delete( + f"/api/v1/roles/{role.id}/members/{user.id}", + headers={"Authorization": token}, + ) + + assert res.status_code == 403 + finally: + await db.execute( + "DELETE FROM userroles WHERE role_id = $1 AND user_id = $2;", + manage_roles_role.id, + user.id, + ) + await db.execute("DELETE FROM roles WHERE id = $1", role.id) + + +@pytest.mark.db +@pytest.mark.asyncio +async def test_update_role_positions_up( + app: AsyncClient, db, user, token, manage_roles_role +): + try: + roles = [] + # manage roles -> 1 -> 3 -> 2 -> 4 + role_names = ["1", "3", "2", "4"] + for role_name in role_names: + query = """ + INSERT INTO roles (id, name, color, permissions, position) + VALUES (create_snowflake(), $1, 0, 0, (SELECT COUNT(*) FROM roles) + 1) + RETURNING *; + """ + role = Role(**await Role.pool.fetchrow(query, role_name)) + roles.append(role) + + await UserRole.create(user.id, manage_roles_role.id) + + res = await app.patch( + f"/api/v1/roles/{roles[2].id}", + json={"position": 3}, + headers={"Authorization": token}, + ) + assert res.status_code == 204 + + res = await app.get("/api/v1/roles") + new_roles = sorted(res.json(), key=lambda x: x["position"]) + + for i, role in enumerate(new_roles, 1): + assert ( + role["position"] == i + ) # make sure roles are ordered with no missing positions + + for i in range(1, 5): + assert new_roles[i]["name"] == str(i) + finally: + await db.execute( + "DELETE FROM userroles WHERE role_id = $1 AND user_id = $2;", + manage_roles_role.id, + user.id, + ) + for role in roles: + await db.execute("DELETE FROM roles WHERE id = $1", role.id) + + +@pytest.mark.db +@pytest.mark.asyncio +async def test_update_role_positions_down( + app: AsyncClient, db, user, token, manage_roles_role +): + try: + roles = [] + # manage roles -> 1 -> 3 -> 2 -> 4 + role_names = ["1", "3", "2", "4"] + for role_name in role_names: + query = """ + INSERT INTO roles (id, name, color, permissions, position) + VALUES (create_snowflake(), $1, 0, 0, (SELECT COUNT(*) FROM roles) + 1) + RETURNING *; + """ + role = Role(**await Role.pool.fetchrow(query, role_name)) + roles.append(role) + + await UserRole.create(user.id, manage_roles_role.id) + + res = await app.patch( + f"/api/v1/roles/{roles[1].id}", + json={"position": 4}, + headers={"Authorization": token}, + ) + assert res.status_code == 204 + + res = await app.get("/api/v1/roles") + new_roles = sorted(res.json(), key=lambda x: x["position"]) + + for i, role in enumerate(new_roles, 1): + assert ( + role["position"] == i + ) # make sure roles are ordered with no missing positions + + for i in range(1, 5): + assert new_roles[i]["name"] == str(i) + finally: + await db.execute( + "DELETE FROM userroles WHERE role_id = $1 AND user_id = $2;", + manage_roles_role.id, + user.id, + ) + for i in roles: + await db.execute("DELETE FROM roles WHERE id = $1", int(role["id"])) From 666d3f2663ae968b527d9db17158ea18c9f91e3c Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 16:00:33 +0200 Subject: [PATCH 11/26] fix test --- tests/test_roles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_roles.py b/tests/test_roles.py index 7284b2e..dfc6dc0 100644 --- a/tests/test_roles.py +++ b/tests/test_roles.py @@ -389,5 +389,5 @@ async def test_update_role_positions_down( manage_roles_role.id, user.id, ) - for i in roles: - await db.execute("DELETE FROM roles WHERE id = $1", int(role["id"])) + for role in roles: + await db.execute("DELETE FROM roles WHERE id = $1", role.id) From 19ef1546160a3cc176499de22f0ab6824c6bf869 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 18:29:49 +0200 Subject: [PATCH 12/26] update user fixture scope --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4cc602e..8928c74 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,13 +37,13 @@ async def db(event_loop) -> bool: await delete_tables() -@pytest.fixture +@pytest.fixture(scope="function") async def user(db): yield await User.create(0, "Test", "0001") await db.execute("""DELETE FROM users WHERE username = 'Test'""") -@pytest.fixture +@pytest.fixture(scope="function") async def token(user, db): yield jwt.encode( {"uid": user.id}, From 6a4154464a067fc4c16623e6762e2c0e3ced393b Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 18:41:50 +0200 Subject: [PATCH 13/26] update auth dependency --- api/dependencies.py | 51 ++++++++++++++++--------- api/versions/v1/routers/roles/routes.py | 14 +++---- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/api/dependencies.py b/api/dependencies.py index 629750b..31ed77e 100644 --- a/api/dependencies.py +++ b/api/dependencies.py @@ -2,36 +2,53 @@ import utils import config -from typing import List, Optional, Union +from api.models import User +from typing import List, Union from fastapi import Depends, HTTPException, Request from api.models import Role from api.models.permissions import BasePermission -async def access_token(request: Request) -> Optional[dict]: - """Attempts to locate and decode JWT token.""" - token = request.headers.get("authorization") +def authorization(app_only: bool = False, user_only: bool = False): + if app_only and user_only: + raise ValueError("can't set both app_only and user_only to True") - if token is None: - raise HTTPException(status_code=401) + async def inner(request: Request): + """Attempts to locate and decode JWT token.""" + token = request.headers.get("authorization") - try: - data = jwt.decode( - jwt=token, - algorithms=["HS256"], - key=config.secret_key(), - ) - except (jwt.PyJWTError, jwt.InvalidSignatureError): - raise HTTPException(status_code=401, detail="Invalid token.") + if token is None: + raise HTTPException(status_code=401) - data["uid"] = int(data["uid"]) + try: + data = jwt.decode( + jwt=token, + algorithms=["HS256"], + key=config.secret_key(), + ) + except (jwt.PyJWTError, jwt.InvalidSignatureError): + raise HTTPException(status_code=401, detail="Invalid token.") + + data["uid"] = int(data["uid"]) + + user = await User.fetch(data["uid"]) + if not user: + raise HTTPException(status_code=401, detail="Invalid token.") + + if app_only and not user.app: + raise HTTPException(status_code=403, detail="Users can't use this endpoint") - return data + if user_only and user.app: + raise HTTPException(status_code=403, detail="Bots can't use this endpoint") + + return data + + return Depends(inner) async def has_permissions(permissions: List[Union[int, BasePermission]]): - async def inner(token=Depends(access_token)): + async def inner(token=authorization()): query = """ WITH user_roles AS ( SELECT role_id FROM userroles WHERE user_id = $1 diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index da9fb10..6f517dd 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -2,10 +2,10 @@ import asyncpg from typing import List, Union -from fastapi import APIRouter, Depends, HTTPException, Query, Response +from fastapi import APIRouter, HTTPException, Query, Response from api.models import Role, UserRole -from api.dependencies import access_token +from api.dependencies import authorization from api.models.permissions import ManageRoles from api.versions.v1.routers.roles.models import ( NewRoleBody, @@ -58,7 +58,7 @@ async def fetch_role(id: int): @router.post("", tags=["roles"], response_model=RoleResponse) -async def create_role(body: NewRoleBody, token=Depends(access_token)): +async def create_role(body: NewRoleBody, token=authorization()): query = """ WITH user_roles AS ( SELECT role_id FROM userroles WHERE user_id = $1 @@ -95,7 +95,7 @@ async def create_role(body: NewRoleBody, token=Depends(access_token)): @router.patch("/{id}", tags=["roles"]) -async def update_role(id: int, body: UpdateRoleBody, token=Depends(access_token)): +async def update_role(id: int, body: UpdateRoleBody, token=authorization()): role = await Role.fetch(id) if not role: raise HTTPException(404, "Role Not Found") @@ -173,7 +173,7 @@ async def update_role(id: int, body: UpdateRoleBody, token=Depends(access_token) @router.delete("/{id}", tags=["roles"]) -async def delete_role(id: int, token=Depends(access_token)): +async def delete_role(id: int, token=authorization()): role = await Role.fetch(id) if not role: raise HTTPException(404, "Role Not Found") @@ -224,7 +224,7 @@ async def delete_role(id: int, token=Depends(access_token)): @router.put("/{role_id}/members/{member_id}", tags=["roles"]) async def add_member_to_role( - role_id: int, member_id: int, token=Depends(access_token) + role_id: int, member_id: int, token=authorization() ) -> Union[Response, utils.JSONResponse]: role = await Role.fetch(role_id) if not role: @@ -262,7 +262,7 @@ async def add_member_to_role( @router.delete("/{role_id}/members/{member_id}", tags=["roles"]) async def remove_member_from_role( - role_id: int, member_id: int, token=Depends(access_token) + role_id: int, member_id: int, token=authorization() ) -> Union[Response, utils.JSONResponse]: role = await Role.fetch(role_id) if not role: From 8042c94a0b8844379034896fa629724f2e88478f Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 18:46:02 +0200 Subject: [PATCH 14/26] reformat queries --- api/dependencies.py | 14 +++++++-- api/versions/v1/routers/roles/routes.py | 40 +++++++++++-------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/api/dependencies.py b/api/dependencies.py index 31ed77e..df2f143 100644 --- a/api/dependencies.py +++ b/api/dependencies.py @@ -50,10 +50,18 @@ async def inner(request: Request): async def has_permissions(permissions: List[Union[int, BasePermission]]): async def inner(token=authorization()): query = """ - WITH user_roles AS ( - SELECT role_id FROM userroles WHERE user_id = $1 + WITH userroles AS ( + SELECT ur.role_id + FROM userroles ur + WHERE ur.user_id = $1 + ) + SELECT r.position + r.permissions + FROM roles r + WHERE r.id IN ( + SELECT role_id + FROM userroles ) - SELECT position, permissions FROM roles WHERE id IN (SELECT * FROM user_roles); """ records = await Role.pool.fetch(query, token["uid"]) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index 6f517dd..5085f2d 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -2,7 +2,7 @@ import asyncpg from typing import List, Union -from fastapi import APIRouter, HTTPException, Query, Response +from fastapi import APIRouter, HTTPException, Response from api.models import Role, UserRole from api.dependencies import authorization @@ -19,18 +19,13 @@ @router.get("", tags=["roles"], response_model=List[RoleResponse]) -async def fetch_all_roles( - limit: int = Query(None), - offset: int = Query(None), -): +async def fetch_all_roles(): query = """ - SELECT - *, id::TEXT - FROM roles - LIMIT $1 - OFFSET $2; + SELECT *, + r.id::TEXT + FROM roles r """ - records = await Role.pool.fetch(query, limit, offset) + records = await Role.pool.fetch(query) return [dict(record) for record in records] @@ -38,19 +33,17 @@ async def fetch_all_roles( @router.get("/{id}", tags=["roles"], response_model=DetailedRoleResponse) async def fetch_role(id: int): query = """ - SELECT - *, - id::TEXT, - COALESCE( - ( - SELECT - json_agg(user_id::TEXT) + SELECT *, + id::TEXT, + COALESCE( + ( + SELECT json_agg(ur.user_id::TEXT) FROM userroles ur - WHERE ur.role_id = $1 - ), '[]' - ) members - FROM roles r - WHERE r.id = $1; + WHERE ur.role_id = r.id + ), '[]' + ) members + FROM roles r + WHERE r.id = $1 """ record = await Role.pool.fetchrow(query, id) @@ -84,6 +77,7 @@ async def create_role(body: NewRoleBody, token=authorization()): VALUES (create_snowflake(), $1, $2, $3, (SELECT COUNT(*) FROM roles) + 1) RETURNING *; """ + try: record = await Role.pool.fetchrow( query, body.name, body.color, body.permissions From 8f54d19857c584a948f0a6a1a9bb800fde0d057a Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 18:51:03 +0200 Subject: [PATCH 15/26] Check if role exists after checking permissions --- api/versions/v1/routers/roles/routes.py | 63 +++++++++++++------------ 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index 5085f2d..ca00eae 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -90,10 +90,6 @@ async def create_role(body: NewRoleBody, token=authorization()): @router.patch("/{id}", tags=["roles"]) async def update_role(id: int, body: UpdateRoleBody, token=authorization()): - role = await Role.fetch(id) - if not role: - raise HTTPException(404, "Role Not Found") - query = """ WITH user_roles AS ( SELECT role_id FROM userroles WHERE user_id = $1 @@ -109,11 +105,15 @@ async def update_role(id: int, body: UpdateRoleBody, token=authorization()): for record in records: user_permissions |= record["permissions"] + if not utils.has_permission(user_permissions, ManageRoles()): + raise HTTPException(403, "Missing Permissions") + + role = await Role.fetch(id) + if not role: + raise HTTPException(404, "Role Not Found") + top_role = min(records, key=lambda record: record["position"]) - if ( - not utils.has_permission(user_permissions, ManageRoles()) - or top_role["position"] >= role.position - ): + if top_role["position"] >= role.position: raise HTTPException(403, "Missing Permissions") data = body.dict(exclude_unset=True) @@ -168,9 +168,6 @@ async def update_role(id: int, body: UpdateRoleBody, token=authorization()): @router.delete("/{id}", tags=["roles"]) async def delete_role(id: int, token=authorization()): - role = await Role.fetch(id) - if not role: - raise HTTPException(404, "Role Not Found") query = """ WITH user_roles AS ( @@ -187,11 +184,15 @@ async def delete_role(id: int, token=authorization()): for record in records: user_permissions |= record["permissions"] + if not utils.has_permission(user_permissions, ManageRoles()): + raise HTTPException(403, "Missing Permissions") + + role = await Role.fetch(id) + if not role: + raise HTTPException(404, "Role Not Found") + top_role = min(records, key=lambda record: record["position"]) - if ( - not utils.has_permission(user_permissions, ManageRoles()) - or top_role["position"] >= role.position - ): + if top_role["position"] >= role.position: raise HTTPException(403, "Missing Permissions") query = """ @@ -220,10 +221,6 @@ async def delete_role(id: int, token=authorization()): async def add_member_to_role( role_id: int, member_id: int, token=authorization() ) -> Union[Response, utils.JSONResponse]: - role = await Role.fetch(role_id) - if not role: - raise HTTPException(404, "Role Not Found") - query = """ WITH user_roles AS ( SELECT role_id FROM userroles WHERE user_id = $1 @@ -239,11 +236,15 @@ async def add_member_to_role( for record in records: user_permissions |= record["permissions"] + if not utils.has_permission(user_permissions, ManageRoles()): + raise HTTPException(403, "Missing Permissions") + + role = await Role.fetch(role_id) + if not role: + raise HTTPException(404, "Role Not Found") + top_role = min(records, key=lambda record: record["position"]) - if ( - not utils.has_permission(user_permissions, ManageRoles()) - or top_role["position"] >= role.position - ): + if top_role["position"] >= role.position: raise HTTPException(403, "Missing Permissions") try: @@ -258,10 +259,6 @@ async def add_member_to_role( async def remove_member_from_role( role_id: int, member_id: int, token=authorization() ) -> Union[Response, utils.JSONResponse]: - role = await Role.fetch(role_id) - if not role: - raise HTTPException(404, "Role Not Found") - query = """ WITH user_roles AS ( SELECT role_id FROM userroles WHERE user_id = $1 @@ -277,11 +274,15 @@ async def remove_member_from_role( for record in records: user_permissions |= record["permissions"] + if not utils.has_permission(user_permissions, ManageRoles()): + raise HTTPException(403, "Missing Permissions") + + role = await Role.fetch(role_id) + if not role: + raise HTTPException(404, "Role Not Found") + top_role = min(records, key=lambda record: record["position"]) - if ( - not utils.has_permission(user_permissions, ManageRoles()) - or top_role["position"] >= role.position - ): + if top_role["position"] >= role.position: raise HTTPException(403, "Missing Permissions") await UserRole.delete(member_id, role_id) From f59a4ff88732585b25b9ba389463ba4ada34c741 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 19:10:41 +0200 Subject: [PATCH 16/26] fix --- api/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/dependencies.py b/api/dependencies.py index df2f143..707542d 100644 --- a/api/dependencies.py +++ b/api/dependencies.py @@ -47,7 +47,7 @@ async def inner(request: Request): return Depends(inner) -async def has_permissions(permissions: List[Union[int, BasePermission]]): +def has_permissions(permissions: List[Union[int, BasePermission]]): async def inner(token=authorization()): query = """ WITH userroles AS ( From 00f55d9cd98b67ce6dd252f83a85589b972d493f Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 19:10:49 +0200 Subject: [PATCH 17/26] update docstring --- utils/permissions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/utils/permissions.py b/utils/permissions.py index cc15f79..2e453e0 100644 --- a/utils/permissions.py +++ b/utils/permissions.py @@ -5,6 +5,7 @@ def has_permissions( permissions: int, required: List[Union[int, BasePermission]] ) -> bool: + """Returns `True` if this role has all provided permissions""" if permissions & Administrator().value: return True @@ -19,6 +20,7 @@ def has_permissions( def has_permission(permissions: int, permission: Union[BasePermission, int]) -> bool: + """Returns `True` if this role has the provided permission""" if permissions & Administrator().value: return True From 0d922ddbe9f782061a54adaf6a3f5308e70c2960 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 19:52:16 +0200 Subject: [PATCH 18/26] docs --- api/app.py | 3 + api/versions/v1/routers/roles/routes.py | 95 ++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/api/app.py b/api/app.py index 1f5c3cb..7168573 100644 --- a/api/app.py +++ b/api/app.py @@ -15,6 +15,9 @@ docs_url="/api/docs", redoc_url="/api/redoc", openapi_url="/api/docs/openapi.json", + openapi_tags=[ + {"name": "roles", "description": "Manage roles"}, + ], ) app.router.prefix = "/api" app.router.default_response_class = JSONResponse diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index ca00eae..75b6f83 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -18,8 +18,10 @@ router = APIRouter(prefix="/roles") -@router.get("", tags=["roles"], response_model=List[RoleResponse]) +@router.get("", tags=["roles"], response_model=List[RoleResponse], status_code=200) async def fetch_all_roles(): + """Fetch all roles""" + query = """ SELECT *, r.id::TEXT @@ -30,8 +32,18 @@ async def fetch_all_roles(): return [dict(record) for record in records] -@router.get("/{id}", tags=["roles"], response_model=DetailedRoleResponse) +@router.get( + "/{id}", + tags=["roles"], + response_model=DetailedRoleResponse, + status_code=200, + responses={ + 404: {"description": "Role not found"}, + }, +) async def fetch_role(id: int): + """Fetch a role by its id""" + query = """ SELECT *, id::TEXT, @@ -47,11 +59,28 @@ async def fetch_role(id: int): """ record = await Role.pool.fetchrow(query, id) + if not record: + raise HTTPException(404, "Role not found") + return dict(record) -@router.post("", tags=["roles"], response_model=RoleResponse) +@router.post( + "", + tags=["roles"], + response_model=RoleResponse, + responses={ + 409: {"description": "Role with that name already exists"}, + 201: {"description": "Role Created Successfully"}, + 403: {"description": "Missing Permissions"}, + 401: {"description": "Unauthorized"}, + 422: {"description": "Invalid body"}, + }, + status_code=201, +) async def create_role(body: NewRoleBody, token=authorization()): + """Create a new role""" + query = """ WITH user_roles AS ( SELECT role_id FROM userroles WHERE user_id = $1 @@ -88,8 +117,22 @@ async def create_role(body: NewRoleBody, token=authorization()): return utils.JSONResponse(status_code=201, content=dict(record)) -@router.patch("/{id}", tags=["roles"]) +@router.patch( + "/{id}", + tags=["roles"], + responses={ + 409: {"description": "Role with that name already exists"}, + 204: {"description": "Role Updated Successfully"}, + 403: {"description": "Missing Permissions"}, + 404: {"description": "Role not found"}, + 401: {"description": "Unauthorized"}, + 422: {"description": "Invalid body"}, + }, + status_code=204, +) async def update_role(id: int, body: UpdateRoleBody, token=authorization()): + """Update role by id""" + query = """ WITH user_roles AS ( SELECT role_id FROM userroles WHERE user_id = $1 @@ -163,11 +206,22 @@ async def update_role(id: int, body: UpdateRoleBody, token=authorization()): await Role.pool.execute(query, id, *data.values()) - return utils.JSONResponse(status_code=204) + return Response(status_code=204, content="") -@router.delete("/{id}", tags=["roles"]) +@router.delete( + "/{id}", + tags=["roles"], + responses={ + 204: {"description": "Role Updated Successfully"}, + 403: {"description": "Missing Permissions"}, + 404: {"description": "Role not found"}, + 401: {"description": "Unauthorized"}, + }, + status_code=204, +) async def delete_role(id: int, token=authorization()): + """Delete role by id""" query = """ WITH user_roles AS ( @@ -214,10 +268,21 @@ async def delete_role(id: int, token=authorization()): """ await Role.pool.execute(query, id) - return utils.JSONResponse(status_code=204) + return Response(status_code=204, content="") -@router.put("/{role_id}/members/{member_id}", tags=["roles"]) +@router.put( + "/{role_id}/members/{member_id}", + tags=["roles"], + responses={ + 204: {"description": "Role assigned to member"}, + 409: {"description": "User already has the role"}, + 404: {"description": "Role or member not found"}, + 403: {"description": "Missing Permissions"}, + 401: {"description": "Unauthorized"}, + }, + status_code=204, +) async def add_member_to_role( role_id: int, member_id: int, token=authorization() ) -> Union[Response, utils.JSONResponse]: @@ -251,11 +316,23 @@ async def add_member_to_role( await UserRole.create(member_id, role_id) except asyncpg.exceptions.UniqueViolationError: raise HTTPException(409, "User already has the role") + except asyncpg.exceptions.ForeignKeyViolationError: + raise HTTPException(404, "Member not found") return Response(status_code=204, content="") -@router.delete("/{role_id}/members/{member_id}", tags=["roles"]) +@router.delete( + "/{role_id}/members/{member_id}", + tags=["roles"], + responses={ + 204: {"description": "Role removed from member"}, + 403: {"description": "Missing Permissions"}, + 404: {"description": "Role not found"}, + 401: {"description": "Unauthorized"}, + }, + status_code=204, +) async def remove_member_from_role( role_id: int, member_id: int, token=authorization() ) -> Union[Response, utils.JSONResponse]: From 4a8ab179103cf9d32389f336a6883528da2ad477 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 19:59:06 +0200 Subject: [PATCH 19/26] reformat queries --- api/dependencies.py | 14 ++-- api/versions/v1/routers/roles/routes.py | 92 ++++++++++++++++++------- 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/api/dependencies.py b/api/dependencies.py index 707542d..40335fe 100644 --- a/api/dependencies.py +++ b/api/dependencies.py @@ -52,15 +52,15 @@ async def inner(token=authorization()): query = """ WITH userroles AS ( SELECT ur.role_id - FROM userroles ur - WHERE ur.user_id = $1 - ) + FROM userroles ur + WHERE ur.user_id = $1 + ) SELECT r.position - r.permissions - FROM roles r - WHERE r.id IN ( + r.permissions + FROM roles r + WHERE r.id IN ( SELECT role_id - FROM userroles + FROM userroles ) """ diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index 75b6f83..d153224 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -45,17 +45,17 @@ async def fetch_role(id: int): """Fetch a role by its id""" query = """ - SELECT *, - id::TEXT, - COALESCE( - ( - SELECT json_agg(ur.user_id::TEXT) - FROM userroles ur - WHERE ur.role_id = r.id - ), '[]' - ) members - FROM roles r - WHERE r.id = $1 + SELECT *, + id::TEXT, + COALESCE( + ( + SELECT json_agg(ur.user_id::TEXT) + FROM userroles ur + WHERE ur.role_id = r.id + ), '[]' + ) members + FROM roles r + WHERE r.id = $1 """ record = await Role.pool.fetchrow(query, id) @@ -82,10 +82,18 @@ async def create_role(body: NewRoleBody, token=authorization()): """Create a new role""" query = """ - WITH user_roles AS ( - SELECT role_id FROM userroles WHERE user_id = $1 + WITH userroles AS ( + SELECT ur.role_id + FROM userroles ur + WHERE ur.user_id = $1 + ) + SELECT r.position + r.permissions + FROM roles r + WHERE r.id IN ( + SELECT role_id + FROM userroles ) - SELECT permissions FROM roles WHERE id IN (SELECT * FROM user_roles); """ records = await Role.pool.fetch(query, token["uid"]) @@ -134,10 +142,18 @@ async def update_role(id: int, body: UpdateRoleBody, token=authorization()): """Update role by id""" query = """ - WITH user_roles AS ( - SELECT role_id FROM userroles WHERE user_id = $1 + WITH userroles AS ( + SELECT ur.role_id + FROM userroles ur + WHERE ur.user_id = $1 + ) + SELECT r.position + r.permissions + FROM roles r + WHERE r.id IN ( + SELECT role_id + FROM userroles ) - SELECT position, permissions FROM roles WHERE id IN (SELECT * FROM user_roles); """ records = await Role.pool.fetch(query, token["uid"]) @@ -224,10 +240,18 @@ async def delete_role(id: int, token=authorization()): """Delete role by id""" query = """ - WITH user_roles AS ( - SELECT role_id FROM userroles WHERE user_id = $1 + WITH userroles AS ( + SELECT ur.role_id + FROM userroles ur + WHERE ur.user_id = $1 + ) + SELECT r.position + r.permissions + FROM roles r + WHERE r.id IN ( + SELECT role_id + FROM userroles ) - SELECT position, permissions FROM roles WHERE id IN (SELECT * FROM user_roles); """ records = await Role.pool.fetch(query, token["uid"]) @@ -287,10 +311,18 @@ async def add_member_to_role( role_id: int, member_id: int, token=authorization() ) -> Union[Response, utils.JSONResponse]: query = """ - WITH user_roles AS ( - SELECT role_id FROM userroles WHERE user_id = $1 + WITH userroles AS ( + SELECT ur.role_id + FROM userroles ur + WHERE ur.user_id = $1 + ) + SELECT r.position + r.permissions + FROM roles r + WHERE r.id IN ( + SELECT role_id + FROM userroles ) - SELECT position, permissions FROM roles WHERE id IN (SELECT * FROM user_roles); """ records = await Role.pool.fetch(query, token["uid"]) @@ -337,10 +369,18 @@ async def remove_member_from_role( role_id: int, member_id: int, token=authorization() ) -> Union[Response, utils.JSONResponse]: query = """ - WITH user_roles AS ( - SELECT role_id FROM userroles WHERE user_id = $1 + WITH userroles AS ( + SELECT ur.role_id + FROM userroles ur + WHERE ur.user_id = $1 + ) + SELECT r.position + r.permissions + FROM roles r + WHERE r.id IN ( + SELECT role_id + FROM userroles ) - SELECT position, permissions FROM roles WHERE id IN (SELECT * FROM user_roles); """ records = await Role.pool.fetch(query, token["uid"]) From 821bddf4f6dfecb252b1f460072e9417044ece5f Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 20:09:14 +0200 Subject: [PATCH 20/26] fix --- api/dependencies.py | 2 +- api/versions/v1/routers/roles/routes.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/dependencies.py b/api/dependencies.py index 40335fe..7a7d1c8 100644 --- a/api/dependencies.py +++ b/api/dependencies.py @@ -55,7 +55,7 @@ async def inner(token=authorization()): FROM userroles ur WHERE ur.user_id = $1 ) - SELECT r.position + SELECT r.position, r.permissions FROM roles r WHERE r.id IN ( diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index d153224..9d0e9c5 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -87,7 +87,7 @@ async def create_role(body: NewRoleBody, token=authorization()): FROM userroles ur WHERE ur.user_id = $1 ) - SELECT r.position + SELECT r.position, r.permissions FROM roles r WHERE r.id IN ( @@ -147,7 +147,7 @@ async def update_role(id: int, body: UpdateRoleBody, token=authorization()): FROM userroles ur WHERE ur.user_id = $1 ) - SELECT r.position + SELECT r.position, r.permissions FROM roles r WHERE r.id IN ( @@ -245,7 +245,7 @@ async def delete_role(id: int, token=authorization()): FROM userroles ur WHERE ur.user_id = $1 ) - SELECT r.position + SELECT r.position, r.permissions FROM roles r WHERE r.id IN ( @@ -316,7 +316,7 @@ async def add_member_to_role( FROM userroles ur WHERE ur.user_id = $1 ) - SELECT r.position + SELECT r.position, r.permissions FROM roles r WHERE r.id IN ( @@ -374,7 +374,7 @@ async def remove_member_from_role( FROM userroles ur WHERE ur.user_id = $1 ) - SELECT r.position + SELECT r.position, r.permissions FROM roles r WHERE r.id IN ( From ced06b8358a0d26bc60c6288653ebce53d089e80 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 20:34:39 +0200 Subject: [PATCH 21/26] reorder responses --- api/versions/v1/routers/roles/routes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index 9d0e9c5..1349653 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -70,10 +70,10 @@ async def fetch_role(id: int): tags=["roles"], response_model=RoleResponse, responses={ - 409: {"description": "Role with that name already exists"}, 201: {"description": "Role Created Successfully"}, - 403: {"description": "Missing Permissions"}, 401: {"description": "Unauthorized"}, + 403: {"description": "Missing Permissions"}, + 409: {"description": "Role with that name already exists"}, 422: {"description": "Invalid body"}, }, status_code=201, @@ -129,11 +129,11 @@ async def create_role(body: NewRoleBody, token=authorization()): "/{id}", tags=["roles"], responses={ - 409: {"description": "Role with that name already exists"}, 204: {"description": "Role Updated Successfully"}, + 401: {"description": "Unauthorized"}, 403: {"description": "Missing Permissions"}, 404: {"description": "Role not found"}, - 401: {"description": "Unauthorized"}, + 409: {"description": "Role with that name already exists"}, 422: {"description": "Invalid body"}, }, status_code=204, @@ -230,9 +230,9 @@ async def update_role(id: int, body: UpdateRoleBody, token=authorization()): tags=["roles"], responses={ 204: {"description": "Role Updated Successfully"}, + 401: {"description": "Unauthorized"}, 403: {"description": "Missing Permissions"}, 404: {"description": "Role not found"}, - 401: {"description": "Unauthorized"}, }, status_code=204, ) @@ -300,10 +300,10 @@ async def delete_role(id: int, token=authorization()): tags=["roles"], responses={ 204: {"description": "Role assigned to member"}, - 409: {"description": "User already has the role"}, - 404: {"description": "Role or member not found"}, - 403: {"description": "Missing Permissions"}, 401: {"description": "Unauthorized"}, + 403: {"description": "Missing Permissions"}, + 404: {"description": "Role or member not found"}, + 409: {"description": "User already has the role"}, }, status_code=204, ) @@ -359,9 +359,9 @@ async def add_member_to_role( tags=["roles"], responses={ 204: {"description": "Role removed from member"}, + 401: {"description": "Unauthorized"}, 403: {"description": "Missing Permissions"}, 404: {"description": "Role not found"}, - 401: {"description": "Unauthorized"}, }, status_code=204, ) From 4c7dc37bbcb151713272c1b318309ce43649d090 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 20:42:54 +0200 Subject: [PATCH 22/26] remove 422 from responses --- api/versions/v1/routers/roles/routes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index 1349653..3fa05d4 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -74,7 +74,6 @@ async def fetch_role(id: int): 401: {"description": "Unauthorized"}, 403: {"description": "Missing Permissions"}, 409: {"description": "Role with that name already exists"}, - 422: {"description": "Invalid body"}, }, status_code=201, ) @@ -134,7 +133,6 @@ async def create_role(body: NewRoleBody, token=authorization()): 403: {"description": "Missing Permissions"}, 404: {"description": "Role not found"}, 409: {"description": "Role with that name already exists"}, - 422: {"description": "Invalid body"}, }, status_code=204, ) From 8aa0a9d6be415a59a9a009f5485dbb300d1dff41 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 20:50:49 +0200 Subject: [PATCH 23/26] update docstring --- utils/permissions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/permissions.py b/utils/permissions.py index 2e453e0..e824932 100644 --- a/utils/permissions.py +++ b/utils/permissions.py @@ -5,7 +5,7 @@ def has_permissions( permissions: int, required: List[Union[int, BasePermission]] ) -> bool: - """Returns `True` if this role has all provided permissions""" + """Returns `True` if `permissions` has all required permissions""" if permissions & Administrator().value: return True @@ -20,7 +20,7 @@ def has_permissions( def has_permission(permissions: int, permission: Union[BasePermission, int]) -> bool: - """Returns `True` if this role has the provided permission""" + """Returns `True` if `permissions` has required permission""" if permissions & Administrator().value: return True From 2cc466af7aa39d2adc07e70870132d7598179916 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Sun, 5 Sep 2021 21:42:41 +0200 Subject: [PATCH 24/26] update dependency --- api/dependencies.py | 17 +-- api/versions/v1/routers/roles/routes.py | 178 ++++-------------------- 2 files changed, 35 insertions(+), 160 deletions(-) diff --git a/api/dependencies.py b/api/dependencies.py index 7a7d1c8..849e8c2 100644 --- a/api/dependencies.py +++ b/api/dependencies.py @@ -50,20 +50,14 @@ async def inner(request: Request): def has_permissions(permissions: List[Union[int, BasePermission]]): async def inner(token=authorization()): query = """ - WITH userroles AS ( + SELECT * + FROM roles r + WHERE r.id IN ( SELECT ur.role_id FROM userroles ur WHERE ur.user_id = $1 - ) - SELECT r.position, - r.permissions - FROM roles r - WHERE r.id IN ( - SELECT role_id - FROM userroles ) """ - records = await Role.pool.fetch(query, token["uid"]) if not records: raise HTTPException(403, "Missing Permissions") @@ -72,6 +66,9 @@ async def inner(token=authorization()): for record in records: user_permissions |= record["permissions"] - return utils.has_permissions(user_permissions, permissions) + if not utils.has_permissions(user_permissions, permissions): + raise HTTPException(403, "Missing Permissions") + + return [Role(**record) for record in records] return Depends(inner) diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index 3fa05d4..759e483 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, HTTPException, Response from api.models import Role, UserRole -from api.dependencies import authorization +from api.dependencies import has_permissions from api.models.permissions import ManageRoles from api.versions.v1.routers.roles.models import ( NewRoleBody, @@ -18,7 +18,7 @@ router = APIRouter(prefix="/roles") -@router.get("", tags=["roles"], response_model=List[RoleResponse], status_code=200) +@router.get("", tags=["roles"], response_model=List[RoleResponse]) async def fetch_all_roles(): """Fetch all roles""" @@ -51,7 +51,7 @@ async def fetch_role(id: int): ( SELECT json_agg(ur.user_id::TEXT) FROM userroles ur - WHERE ur.role_id = r.id + WHERE ur.role_id = r.id ), '[]' ) members FROM roles r @@ -77,35 +77,13 @@ async def fetch_role(id: int): }, status_code=201, ) -async def create_role(body: NewRoleBody, token=authorization()): +async def create_role(body: NewRoleBody, roles=has_permissions([ManageRoles()])): """Create a new role""" - - query = """ - WITH userroles AS ( - SELECT ur.role_id - FROM userroles ur - WHERE ur.user_id = $1 - ) - SELECT r.position, - r.permissions - FROM roles r - WHERE r.id IN ( - SELECT role_id - FROM userroles - ) - """ - - records = await Role.pool.fetch(query, token["uid"]) - if not records: - raise HTTPException(403, "Missing Permissions") - user_permissions = 0 - for record in records: - user_permissions |= record["permissions"] + for role in roles: + user_permissions |= role.permissions - if not utils.has_permission( - user_permissions, body.permissions - ) or not utils.has_permission(user_permissions, ManageRoles()): + if not utils.has_permission(user_permissions, body.permissions): raise HTTPException(403, "Missing Permissions") query = """ @@ -136,41 +114,21 @@ async def create_role(body: NewRoleBody, token=authorization()): }, status_code=204, ) -async def update_role(id: int, body: UpdateRoleBody, token=authorization()): - """Update role by id""" - - query = """ - WITH userroles AS ( - SELECT ur.role_id - FROM userroles ur - WHERE ur.user_id = $1 - ) - SELECT r.position, - r.permissions - FROM roles r - WHERE r.id IN ( - SELECT role_id - FROM userroles - ) - """ - - records = await Role.pool.fetch(query, token["uid"]) - if not records: - raise HTTPException(403, "Missing Permissions") - - user_permissions = 0 - for record in records: - user_permissions |= record["permissions"] - - if not utils.has_permission(user_permissions, ManageRoles()): - raise HTTPException(403, "Missing Permissions") - +async def update_role( + id: int, + body: UpdateRoleBody, + roles=has_permissions([ManageRoles()]), +): role = await Role.fetch(id) if not role: raise HTTPException(404, "Role Not Found") - top_role = min(records, key=lambda record: record["position"]) - if top_role["position"] >= role.position: + user_permissions = 0 + for r in roles: + user_permissions |= r.permissions + + top_role = min(roles, key=lambda role: role.position) + if top_role.position >= role.position: raise HTTPException(403, "Missing Permissions") data = body.dict(exclude_unset=True) @@ -186,7 +144,7 @@ async def update_role(id: int, body: UpdateRoleBody, token=authorization()): if ( position := data.pop("position", None) ) is not None and position != role.position: - if position <= top_role["position"]: + if position <= top_role.position: raise HTTPException(403, "Missing Permissions") if position > role.position: @@ -234,41 +192,13 @@ async def update_role(id: int, body: UpdateRoleBody, token=authorization()): }, status_code=204, ) -async def delete_role(id: int, token=authorization()): - """Delete role by id""" - - query = """ - WITH userroles AS ( - SELECT ur.role_id - FROM userroles ur - WHERE ur.user_id = $1 - ) - SELECT r.position, - r.permissions - FROM roles r - WHERE r.id IN ( - SELECT role_id - FROM userroles - ) - """ - - records = await Role.pool.fetch(query, token["uid"]) - if not records: - raise HTTPException(403, "Missing Permissions") - - user_permissions = 0 - for record in records: - user_permissions |= record["permissions"] - - if not utils.has_permission(user_permissions, ManageRoles()): - raise HTTPException(403, "Missing Permissions") - +async def delete_role(id: int, roles=has_permissions([ManageRoles()])): role = await Role.fetch(id) if not role: raise HTTPException(404, "Role Not Found") - top_role = min(records, key=lambda record: record["position"]) - if top_role["position"] >= role.position: + top_role = min(roles, key=lambda role: role.position) + if top_role.position >= role.position: raise HTTPException(403, "Missing Permissions") query = """ @@ -306,40 +236,14 @@ async def delete_role(id: int, token=authorization()): status_code=204, ) async def add_member_to_role( - role_id: int, member_id: int, token=authorization() + role_id: int, member_id: int, roles=has_permissions([ManageRoles()]) ) -> Union[Response, utils.JSONResponse]: - query = """ - WITH userroles AS ( - SELECT ur.role_id - FROM userroles ur - WHERE ur.user_id = $1 - ) - SELECT r.position, - r.permissions - FROM roles r - WHERE r.id IN ( - SELECT role_id - FROM userroles - ) - """ - - records = await Role.pool.fetch(query, token["uid"]) - if not records: - raise HTTPException(403, "Missing Permissions") - - user_permissions = 0 - for record in records: - user_permissions |= record["permissions"] - - if not utils.has_permission(user_permissions, ManageRoles()): - raise HTTPException(403, "Missing Permissions") - role = await Role.fetch(role_id) if not role: raise HTTPException(404, "Role Not Found") - top_role = min(records, key=lambda record: record["position"]) - if top_role["position"] >= role.position: + top_role = min(roles, key=lambda role: role.position) + if top_role.position >= role.position: raise HTTPException(403, "Missing Permissions") try: @@ -364,40 +268,14 @@ async def add_member_to_role( status_code=204, ) async def remove_member_from_role( - role_id: int, member_id: int, token=authorization() + role_id: int, member_id: int, roles=has_permissions([ManageRoles()]) ) -> Union[Response, utils.JSONResponse]: - query = """ - WITH userroles AS ( - SELECT ur.role_id - FROM userroles ur - WHERE ur.user_id = $1 - ) - SELECT r.position, - r.permissions - FROM roles r - WHERE r.id IN ( - SELECT role_id - FROM userroles - ) - """ - - records = await Role.pool.fetch(query, token["uid"]) - if not records: - raise HTTPException(403, "Missing Permissions") - - user_permissions = 0 - for record in records: - user_permissions |= record["permissions"] - - if not utils.has_permission(user_permissions, ManageRoles()): - raise HTTPException(403, "Missing Permissions") - role = await Role.fetch(role_id) if not role: raise HTTPException(404, "Role Not Found") - top_role = min(records, key=lambda record: record["position"]) - if top_role["position"] >= role.position: + top_role = min(roles, key=lambda role: role.position) + if top_role.position >= role.position: raise HTTPException(403, "Missing Permissions") await UserRole.delete(member_id, role_id) From e11b8007bc158ccbdf9b30a26fb5fff61cf8ebe6 Mon Sep 17 00:00:00 2001 From: M7MD <55202038+mohamed040406@users.noreply.github.com> Date: Sun, 5 Sep 2021 22:32:36 +0200 Subject: [PATCH 25/26] update this --- api/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/dependencies.py b/api/dependencies.py index 849e8c2..905c845 100644 --- a/api/dependencies.py +++ b/api/dependencies.py @@ -12,7 +12,7 @@ def authorization(app_only: bool = False, user_only: bool = False): if app_only and user_only: - raise ValueError("can't set both app_only and user_only to True") + raise ValueError("app_only and user_only are mutually exclusive") async def inner(request: Request): """Attempts to locate and decode JWT token.""" From 403d39c897fc86ca02d59c032bc9c4ec5edeccd7 Mon Sep 17 00:00:00 2001 From: mohamed040406 Date: Mon, 6 Sep 2021 13:24:53 +0200 Subject: [PATCH 26/26] commit --- api/dependencies.py | 8 ++++---- api/versions/v1/routers/roles/routes.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/dependencies.py b/api/dependencies.py index 849e8c2..9ea54bd 100644 --- a/api/dependencies.py +++ b/api/dependencies.py @@ -27,7 +27,7 @@ async def inner(request: Request): algorithms=["HS256"], key=config.secret_key(), ) - except (jwt.PyJWTError, jwt.InvalidSignatureError): + except jwt.PyJWTError: raise HTTPException(status_code=401, detail="Invalid token.") data["uid"] = int(data["uid"]) @@ -42,13 +42,13 @@ async def inner(request: Request): if user_only and user.app: raise HTTPException(status_code=403, detail="Bots can't use this endpoint") - return data + return user return Depends(inner) def has_permissions(permissions: List[Union[int, BasePermission]]): - async def inner(token=authorization()): + async def inner(user=authorization()): query = """ SELECT * FROM roles r @@ -58,7 +58,7 @@ async def inner(token=authorization()): WHERE ur.user_id = $1 ) """ - records = await Role.pool.fetch(query, token["uid"]) + records = await Role.pool.fetch(query, user.id) if not records: raise HTTPException(403, "Missing Permissions") diff --git a/api/versions/v1/routers/roles/routes.py b/api/versions/v1/routers/roles/routes.py index 759e483..4beca11 100644 --- a/api/versions/v1/routers/roles/routes.py +++ b/api/versions/v1/routers/roles/routes.py @@ -36,7 +36,6 @@ async def fetch_all_roles(): "/{id}", tags=["roles"], response_model=DetailedRoleResponse, - status_code=200, responses={ 404: {"description": "Role not found"}, }, @@ -50,8 +49,8 @@ async def fetch_role(id: int): COALESCE( ( SELECT json_agg(ur.user_id::TEXT) - FROM userroles ur - WHERE ur.role_id = r.id + FROM userroles ur + WHERE ur.role_id = r.id ), '[]' ) members FROM roles r @@ -78,7 +77,7 @@ async def fetch_role(id: int): status_code=201, ) async def create_role(body: NewRoleBody, roles=has_permissions([ManageRoles()])): - """Create a new role""" + # Check if the user has administrator permission or all the permissions provided in the role user_permissions = 0 for role in roles: user_permissions |= role.permissions @@ -123,6 +122,7 @@ async def update_role( if not role: raise HTTPException(404, "Role Not Found") + # Check if the user has administrator permission or all the permissions provided in the role user_permissions = 0 for r in roles: user_permissions |= r.permissions