Skip to content

Commit e3491f7

Browse files
committed
add basic auth
1 parent cfd582b commit e3491f7

File tree

6 files changed

+103
-8
lines changed

6 files changed

+103
-8
lines changed

src/routers/member.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""Member endpoints."""
22

33
import re
4+
from typing import Annotated
45
from uuid import UUID
56

6-
from fastapi import APIRouter, Response, UploadFile, status
7+
from fastapi import APIRouter, Depends, Response, UploadFile, status
8+
9+
from security import authorize
710

811
from ..models.member import Member
912
from ..services.member import MemberService
@@ -13,22 +16,28 @@
1316

1417

1518
@router.post("", response_model=Member)
16-
def create_member_folder(member: Member):
19+
def create_member_folder(
20+
member: Member, authorized: Annotated[None, Depends(authorize)]
21+
):
1722
"""
1823
Create a folder structure for a member and return the member object.
1924
:param member: Member object
25+
:param authorized: fastapi dependency to authorize the request
2026
:return: 200 and the original member object
2127
"""
2228
service.create_folder_structure(member)
2329
return member
2430

2531

2632
@router.put("", response_model=Member)
27-
def update_member_folder(member: Member):
33+
def update_member_folder(
34+
member: Member, authorized: Annotated[None, Depends(authorize)]
35+
):
2836
"""
2937
Update the folder structure for a member and return the member object.
3038
If the member does not exist, return a 404.
3139
:param member: Member object
40+
:param authorized: fastapi dependency to authorize the request
3241
:return: 200 and the original member object
3342
"""
3443
if not service.to_id_path(member.id).exists():
@@ -38,14 +47,17 @@ def update_member_folder(member: Member):
3847

3948

4049
@router.post("/{member_id}/profilePicture", response_model=UUID)
41-
async def upload_member_picture(member_id: UUID, file: UploadFile):
50+
async def upload_member_picture(
51+
member_id: UUID, file: UploadFile, authorized: Annotated[None, Depends(authorize)]
52+
):
4253
"""
4354
Upload a picture for a member to convert
4455
and store the profile picture in different formats
4556
If the member does not exist, return a 404.
4657
If the file is not an image, return a 500.
4758
:param member_id: the id of the member
4859
:param file: the image file
60+
:param authorized: fastapi dependency to authorize the request
4961
:return: 200 and the original member_id
5062
"""
5163
# pylint: disable=duplicate-code

src/routers/video.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""Video endpoints"""
22

33
import re
4+
from typing import Annotated
45
from uuid import UUID
56

6-
from fastapi import APIRouter, Response, UploadFile, status
7+
from fastapi import APIRouter, Depends, Response, UploadFile, status
8+
9+
from security import authorize
710

811
from ..models.video import Video
912
from ..services.video import VideoService
@@ -13,22 +16,24 @@
1316

1417

1518
@router.post("", response_model=Video)
16-
def create_video_folder(video: Video):
19+
def create_video_folder(video: Video, authorized: Annotated[None, Depends(authorize)]):
1720
"""
1821
Create a folder structure for a video and return the video object.
1922
:param video: Video object
23+
:param authorized: fastapi dependency to authorize the request
2024
:return: 200 and the original video object
2125
"""
2226
service.create_folder_structure(video)
2327
return video
2428

2529

2630
@router.put("", response_model=Video)
27-
def update_video_folder(video: Video):
31+
def update_video_folder(video: Video, authorized: Annotated[None, Depends(authorize)]):
2832
"""
2933
Update the folder structure for a video and return the video object.
3034
If the video does not exist, return a 404.
3135
:param video: Video object
36+
:param authorized: fastapi dependency to authorize the request
3237
:return: 200 and the original video object
3338
"""
3439
if not service.to_id_path(video.id).exists():
@@ -38,14 +43,17 @@ def update_video_folder(video: Video):
3843

3944

4045
@router.post("/{video_id}/thumbnail", response_model=UUID)
41-
async def upload_video_poster(video_id: UUID, file: UploadFile):
46+
async def upload_video_poster(
47+
video_id: UUID, file: UploadFile, authorized: Annotated[None, Depends(authorize)]
48+
):
4249
"""
4350
Upload a picture for a video thumbnail to convert
4451
and store the thumbnail in different formats
4552
If the video does not exist, return a 404.
4653
If the file is not an image, return a 500.
4754
:param video_id: the id of the video
4855
:param file: the image file
56+
:param authorized: fastapi dependency to authorize the request
4957
:return: 200 and the original video_id
5058
"""
5159
# pylint: disable=duplicate-code

src/security.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import secrets
2+
from typing import Annotated
3+
4+
from fastapi import Depends, HTTPException, status
5+
from fastapi.security import HTTPBasic, HTTPBasicCredentials
6+
7+
from settings import settings
8+
9+
security = HTTPBasic()
10+
11+
12+
def authorize(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
13+
current_username_bytes = credentials.username.encode("utf8")
14+
correct_username_bytes = settings.username.encode("utf8")
15+
is_correct_username = secrets.compare_digest(
16+
current_username_bytes, correct_username_bytes
17+
)
18+
current_password_bytes = credentials.password.encode("utf8")
19+
correct_password_bytes = settings.password.encode("utf8")
20+
is_correct_password = secrets.compare_digest(
21+
current_password_bytes, correct_password_bytes
22+
)
23+
if not (is_correct_username and is_correct_password):
24+
raise HTTPException(
25+
status_code=status.HTTP_401_UNAUTHORIZED,
26+
detail="Incorrect username or password",
27+
headers={"WWW-Authenticate": "Basic"},
28+
)

src/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class Settings(BaseSettings):
77
"""Class for reading settings from environment variables."""
88

99
server_base_path: str = "./assets/"
10+
username: str = "admin"
11+
password: str = "password"
1012

1113

1214
settings = Settings()

tests/test_security.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import pytest
2+
from fastapi import Depends, FastAPI
3+
from fastapi.security import HTTPBasicCredentials
4+
from fastapi.testclient import TestClient
5+
6+
from src.security import authorize
7+
8+
9+
@pytest.fixture
10+
def client():
11+
app = FastAPI()
12+
13+
@app.get("/secure-endpoint")
14+
def secure_endpoint(credentials: HTTPBasicCredentials = Depends(authorize)):
15+
return {"message": "You are authorized"}
16+
17+
return TestClient(app)
18+
19+
20+
def test_authorize_correct_credentials(mocker, client):
21+
credentials = HTTPBasicCredentials(username="admin", password="password")
22+
mocker.patch("src.security.security", return_value=credentials)
23+
24+
response = client.get("/secure-endpoint", auth=("admin", "password"))
25+
26+
assert response.status_code == 200
27+
assert response.json() == {"message": "You are authorized"}
28+
29+
30+
def test_authorize_incorrect_credentials(mocker, client):
31+
credentials = HTTPBasicCredentials(username="admin", password="password")
32+
mocker.patch("src.security.security", return_value=credentials)
33+
34+
response = client.get("/secure-endpoint", auth=("wrong", "wrong"))
35+
36+
assert response.status_code == 401
37+
assert response.json() == {"detail": "Incorrect username or password"}

tests/test_settings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from src.settings import Settings
2+
3+
4+
def test_settings_default_values():
5+
settings = Settings()
6+
assert settings.server_base_path == "./assets/"
7+
assert settings.username == "admin"
8+
assert settings.password == "password"

0 commit comments

Comments
 (0)