Skip to content
This repository has been archived by the owner on May 24, 2022. It is now read-only.

Commit

Permalink
Merge pull request #157 from SELab-2/use_auth
Browse files Browse the repository at this point in the history
Use authentication in routes
  • Loading branch information
stijndcl authored Mar 24, 2022
2 parents a6d41e4 + eff1427 commit 332f276
Show file tree
Hide file tree
Showing 15 changed files with 406 additions and 155 deletions.
12 changes: 7 additions & 5 deletions backend/src/app/routers/editions/editions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

# Don't add the "Editions" tag here, because then it gets applied
# to all child routes as well
from ...utils.dependencies import require_admin, require_auth, require_coach

editions_router = APIRouter(prefix="/editions")

# Register all child routers
Expand All @@ -31,20 +33,20 @@
editions_router.include_router(router, prefix="/{edition_id}")


@editions_router.get("/",response_model=EditionList, tags=[Tags.EDITIONS])
@editions_router.get("/", response_model=EditionList, tags=[Tags.EDITIONS], dependencies=[Depends(require_auth)])
async def get_editions(db: Session = Depends(get_session)):
"""Get a list of all editions.
Args:
db (Session, optional): connection with the database. Defaults to Depends(get_session).
Returns:
EditionList: an object with a list of all the editions.
"""
# TODO only return editions the user can see
return logic_editions.get_editions(db)


@editions_router.get("/{edition_id}", response_model=Edition, tags=[Tags.EDITIONS])
@editions_router.get("/{edition_id}", response_model=Edition, tags=[Tags.EDITIONS], dependencies=[Depends(require_coach)])
async def get_edition_by_id(edition_id: int, db: Session = Depends(get_session)):
"""Get a specific edition.
Expand All @@ -58,7 +60,7 @@ async def get_edition_by_id(edition_id: int, db: Session = Depends(get_session))
return logic_editions.get_edition_by_id(db, edition_id)


@editions_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Edition, tags=[Tags.EDITIONS])
@editions_router.post("/", status_code=status.HTTP_201_CREATED, response_model=Edition, tags=[Tags.EDITIONS], dependencies=[Depends(require_admin)])
async def post_edition(edition: EditionBase, db: Session = Depends(get_session)):
""" Create a new edition.
Expand All @@ -71,7 +73,7 @@ async def post_edition(edition: EditionBase, db: Session = Depends(get_session))
return logic_editions.create_edition(db, edition)


@editions_router.delete("/{edition_id}", status_code=status.HTTP_204_NO_CONTENT, tags=[Tags.EDITIONS])
@editions_router.delete("/{edition_id}", status_code=status.HTTP_204_NO_CONTENT, tags=[Tags.EDITIONS], dependencies=[Depends(require_admin)])
async def delete_edition(edition_id: int, db: Session = Depends(get_session)):
"""Delete an existing edition.
Expand Down
13 changes: 8 additions & 5 deletions backend/src/app/routers/editions/invites/invites.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,33 @@
from src.app.logic.invites import create_mailto_link, delete_invite_link, get_pending_invites_list
from src.app.routers.tags import Tags
from src.app.schemas.invites import InvitesListResponse, EmailAddress, MailtoLink, InviteLink as InviteLinkModel
from src.app.utils.dependencies import get_edition, get_invite_link
from src.app.utils.dependencies import get_edition, get_invite_link, require_admin
from src.database.database import get_session
from src.database.models import Edition, InviteLink as InviteLinkDB

invites_router = APIRouter(prefix="/invites", tags=[Tags.INVITES])


@invites_router.get("/", response_model=InvitesListResponse)
@invites_router.get("/", response_model=InvitesListResponse, dependencies=[Depends(require_admin)])
async def get_invites(db: Session = Depends(get_session), edition: Edition = Depends(get_edition)):
"""
Get a list of all pending invitation links.
"""
return get_pending_invites_list(db, edition)


@invites_router.post("/", status_code=status.HTTP_201_CREATED, response_model=MailtoLink)
async def create_invite(email: EmailAddress, db: Session = Depends(get_session), edition: Edition = Depends(get_edition)):
@invites_router.post("/", status_code=status.HTTP_201_CREATED, response_model=MailtoLink,
dependencies=[Depends(require_admin)])
async def create_invite(email: EmailAddress, db: Session = Depends(get_session),
edition: Edition = Depends(get_edition)):
"""
Create a new invitation link for the current edition.
"""
return create_mailto_link(db, edition, email)


@invites_router.delete("/{invite_uuid}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response)
@invites_router.delete("/{invite_uuid}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response,
dependencies=[Depends(require_admin)])
async def delete_invite(invite_link: InviteLinkDB = Depends(get_invite_link), db: Session = Depends(get_session)):
"""
Delete an existing invitation link manually so that it can't be used anymore.
Expand Down
8 changes: 4 additions & 4 deletions backend/src/app/routers/editions/webhooks/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from src.database.crud.webhooks import get_webhook, create_webhook
from src.app.schemas.webhooks import WebhookEvent, WebhookUrlResponse
from src.database.models import Edition
from src.app.utils.dependencies import get_edition
from src.app.utils.dependencies import get_edition, require_admin
from src.app.routers.tags import Tags
from src.app.logic.webhooks import process_webhook
from starlette import status
Expand All @@ -18,10 +18,10 @@ def valid_uuid(uuid: str, database: Session = Depends(get_session)):
get_webhook(database, uuid)


# TODO: check admin permission
@webhooks_router.post("/", response_model=WebhookUrlResponse, status_code=status.HTTP_201_CREATED)
@webhooks_router.post("/", response_model=WebhookUrlResponse, status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_admin)])
def new(edition: Edition = Depends(get_edition), database: Session = Depends(get_session)):
"""Create e new webhook for an edition"""
"""Create a new webhook for an edition"""
return create_webhook(database, edition)


Expand Down
14 changes: 6 additions & 8 deletions backend/src/app/routers/skills/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@
from sqlalchemy.orm import Session
from starlette import status

from src.database.database import get_session
from src.app.schemas.skills import SkillBase, Skill, SkillList
from src.app.logic import skills as logic_skills

from src.app.schemas.skills import SkillBase
from src.app.routers.tags import Tags

from src.app.schemas.skills import SkillBase, Skill, SkillList
from src.app.utils.dependencies import require_auth
from src.database.database import get_session

skills_router = APIRouter(prefix="/skills", tags=[Tags.SKILLS])


@skills_router.get("/", response_model=SkillList, tags=[Tags.SKILLS])
@skills_router.get("/", response_model=SkillList, tags=[Tags.SKILLS], dependencies=[Depends(require_auth)])
async def get_skills(db: Session = Depends(get_session)):
"""Get a list of all the base skills that can be added to a student or project.
Expand All @@ -26,7 +24,7 @@ async def get_skills(db: Session = Depends(get_session)):
return logic_skills.get_skills(db)


@skills_router.post("/",status_code=status.HTTP_201_CREATED, response_model=Skill, tags=[Tags.SKILLS])
@skills_router.post("/",status_code=status.HTTP_201_CREATED, response_model=Skill, tags=[Tags.SKILLS], dependencies=[Depends(require_auth)])
async def create_skill(skill: SkillBase, db: Session = Depends(get_session)):
"""Add a new skill into the database.
Expand All @@ -40,7 +38,7 @@ async def create_skill(skill: SkillBase, db: Session = Depends(get_session)):
return logic_skills.create_skill(db, skill)


@skills_router.delete("/{skill_id}", status_code=status.HTTP_204_NO_CONTENT, tags=[Tags.SKILLS])
@skills_router.delete("/{skill_id}", status_code=status.HTTP_204_NO_CONTENT, tags=[Tags.SKILLS], dependencies=[Depends(require_auth)])
async def delete_skill(skill_id: int, db: Session = Depends(get_session)):
"""Delete an existing skill.
Expand Down
15 changes: 8 additions & 7 deletions backend/src/app/routers/users/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
from src.app.routers.tags import Tags
import src.app.logic.users as logic
from src.app.schemas.users import UsersListResponse, AdminPatch, UserRequestsResponse
from src.app.utils.dependencies import require_admin
from src.database.database import get_session

users_router = APIRouter(prefix="/users", tags=[Tags.USERS])


@users_router.get("/", response_model=UsersListResponse)
@users_router.get("/", response_model=UsersListResponse, dependencies=[Depends(require_admin)])
async def get_users(admin: bool = Query(False), edition: int | None = Query(None), db: Session = Depends(get_session)):
"""
Get users
Expand All @@ -18,7 +19,7 @@ async def get_users(admin: bool = Query(False), edition: int | None = Query(None
return logic.get_users_list(db, admin, edition)


@users_router.patch("/{user_id}", status_code=204)
@users_router.patch("/{user_id}", status_code=204, dependencies=[Depends(require_admin)])
async def patch_admin_status(user_id: int, admin: AdminPatch, db: Session = Depends(get_session)):
"""
Set admin-status of user
Expand All @@ -27,7 +28,7 @@ async def patch_admin_status(user_id: int, admin: AdminPatch, db: Session = Depe
logic.edit_admin_status(db, user_id, admin)


@users_router.post("/{user_id}/editions/{edition_id}", status_code=204)
@users_router.post("/{user_id}/editions/{edition_id}", status_code=204, dependencies=[Depends(require_admin)])
async def add_to_edition(user_id: int, edition_id: int, db: Session = Depends(get_session)):
"""
Add user as coach of the given edition
Expand All @@ -36,7 +37,7 @@ async def add_to_edition(user_id: int, edition_id: int, db: Session = Depends(ge
logic.add_coach(db, user_id, edition_id)


@users_router.delete("/{user_id}/editions/{edition_id}", status_code=204)
@users_router.delete("/{user_id}/editions/{edition_id}", status_code=204, dependencies=[Depends(require_admin)])
async def remove_from_edition(user_id: int, edition_id: int, db: Session = Depends(get_session)):
"""
Remove user as coach of the given edition
Expand All @@ -45,7 +46,7 @@ async def remove_from_edition(user_id: int, edition_id: int, db: Session = Depen
logic.remove_coach(db, user_id, edition_id)


@users_router.get("/requests", response_model=UserRequestsResponse)
@users_router.get("/requests", response_model=UserRequestsResponse, dependencies=[Depends(require_admin)])
async def get_requests(edition: int | None = Query(None), db: Session = Depends(get_session)):
"""
Get pending userrequests
Expand All @@ -54,7 +55,7 @@ async def get_requests(edition: int | None = Query(None), db: Session = Depends(
return logic.get_request_list(db, edition)


@users_router.post("/requests/{request_id}/accept", status_code=204)
@users_router.post("/requests/{request_id}/accept", status_code=204, dependencies=[Depends(require_admin)])
async def accept_request(request_id: int, db: Session = Depends(get_session)):
"""
Accept a coach request
Expand All @@ -63,7 +64,7 @@ async def accept_request(request_id: int, db: Session = Depends(get_session)):
logic.accept_request(db, request_id)


@users_router.post("/requests/{request_id}/reject", status_code=204)
@users_router.post("/requests/{request_id}/reject", status_code=204, dependencies=[Depends(require_admin)])
async def reject_request(request_id: int, db: Session = Depends(get_session)):
"""
Reject a coach request
Expand Down
39 changes: 32 additions & 7 deletions backend/src/app/utils/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ def get_edition(edition_id: int, database: Session = Depends(get_session)) -> Ed
async def get_current_active_user(db: Session = Depends(get_session), token: str = Depends(oauth2_scheme)) -> User:
"""Check which user is making a request by decoding its token
This function is used as a dependency for other functions
TODO check if user has any pending coach requests
requires coach request logic to be done
"""
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
Expand All @@ -47,11 +45,23 @@ async def get_current_active_user(db: Session = Depends(get_session), token: str
raise InvalidCredentialsException() from jwt_err


# Alias that is easier to read in the dependency list when
# the return value isn't required
# Require the user to be authorized, coach or admin doesn't matter
require_authorization = get_current_active_user
require_auth = get_current_active_user
async def require_auth(user: User = Depends(get_current_active_user)) -> User:
"""Dependency to check if a user is at least a coach
This dependency should be used to check for resources that aren't linked to
editions
The function checks if the user is either an admin, or a coach with at least
one UserRole (meaning they have been accepted for at least one edition)
"""
# Admins can see everything
if user.admin:
return user

# Coach is not in any editions (yet)
if len(user.editions) == 0:
raise MissingPermissionsException()

return user


async def require_admin(user: User = Depends(get_current_active_user)) -> User:
Expand All @@ -62,6 +72,21 @@ async def require_admin(user: User = Depends(get_current_active_user)) -> User:
return user


async def require_coach(edition: Edition = Depends(get_edition), user: User = Depends(get_current_active_user)) -> User:
"""Dependency to check if a user can see a given resource
This comes down to checking if a coach is linked to an edition or not
"""
# Admins can see everything in any edition
if user.admin:
return user

# Coach is not part of this edition
if edition not in user.editions:
raise MissingPermissionsException()

return user


def get_invite_link(invite_uuid: str, db: Session = Depends(get_session)) -> InviteLink:
"""Get an invite link from the database, given the id in the path"""
return get_invite_link_by_uuid(db, invite_uuid)
17 changes: 17 additions & 0 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from src.database.database import get_session
from src.database.engine import engine

from tests.utils.authorization import AuthClient


@pytest.fixture(scope="session")
def tables():
Expand Down Expand Up @@ -55,3 +57,18 @@ def override_get_session() -> Generator[Session, None, None]:
# Replace get_session with a call to this method instead
app.dependency_overrides[get_session] = override_get_session
return TestClient(app)


@pytest.fixture
def auth_client(database_session: Session) -> AuthClient:
"""Fixture to get a TestClient that handles authentication"""

def override_get_session() -> Generator[Session, None, None]:
"""Inner function to override the Session used in the app
A session provided by a fixture will be used instead
"""
yield database_session

# Replace get_session with a call to this method instead
app.dependency_overrides[get_session] = override_get_session
return AuthClient(database_session, app)
Loading

0 comments on commit 332f276

Please sign in to comment.