Skip to content

Commit

Permalink
🔒️(api) securing API with LTI token
Browse files Browse the repository at this point in the history
API was unsecure and anyone could make request to it from a browser.
Securing it with a LTI token verification on each endpoint.
  • Loading branch information
wilbrdt committed May 30, 2024
1 parent b3b22ba commit 40481b8
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 82 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to

## Unreleased

### Changed

- Secure API endpoints with LTI token

## [0.2.0] - 2024-05-23

### Changed
Expand Down
1 change: 1 addition & 0 deletions src/api/plugins/tdbp/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest
from warren.tests.fixtures.app import http_client
from warren.tests.fixtures.asynchronous import anyio_backend
from warren.tests.fixtures.auth import auth_headers
from warren.tests.fixtures.db import (
db_engine,
db_session,
Expand Down
171 changes: 132 additions & 39 deletions src/api/plugins/tdbp/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for the TdBP Warren plugin."""

from datetime import date, datetime

import httpx
Expand All @@ -13,7 +14,6 @@
"query_params",
[
{}, # Missing parameters
{"course_id": 1234}, # Wrong "course_id" parameter data type
{
"course_id": "course_id_101",
"until": "today",
Expand All @@ -23,24 +23,33 @@
],
)
async def test_api_sliding_window_with_invalid_params(
query_params, http_client: httpx.AsyncClient
query_params, http_client: httpx.AsyncClient, auth_headers: dict
):
"""Test '/window' indicator with invalid query parameters returns 422 HTTP code."""
response = await http_client.get("/api/v1/tdbp/window", params=query_params)
"""Test `/window` endpoint with invalid query parameters returns 422 HTTP code."""
response = await http_client.get(
"/api/v1/tdbp/window",
params=query_params,
headers=auth_headers,
)

assert response.status_code == 422


@pytest.mark.anyio
async def test_api_sliding_window_with_valid_params(
http_client: httpx.AsyncClient, db_session, sliding_window_fake_dataset
http_client: httpx.AsyncClient,
auth_headers: dict,
db_session,
sliding_window_fake_dataset,
):
"""Test '/window' indicator with query parameters returns correct output."""
"""Test `/window` endpoint with query parameters returns correct output."""
course_id = "https://fake-lms.com/course/tdbp_101"
date_until = datetime.now().date()

response = await http_client.get(
"/api/v1/tdbp/window", params={"course_id": course_id, "until": date_until}
"/api/v1/tdbp/window",
params={"course_id": course_id, "until": date_until},
headers=auth_headers,
)

assert response.status_code == 200
Expand All @@ -51,12 +60,28 @@ async def test_api_sliding_window_with_valid_params(
pytest.fail(f"Sliding window indicator is invalid: {err}")


@pytest.mark.anyio
async def test_api_sliding_window_with_invalid_auth_headers(
http_client: httpx.AsyncClient,
):
"""Test `/window` endpoint with an invalid `auth_headers`."""
course_id = "https://fake-lms.com/course/tdbp_101"
date_until = datetime.now().date()
response = await http_client.get(
"/api/v1/tdbp/window",
params={"course_id": course_id, "until": date_until},
headers={"Authorization": "Bearer Wrong_Token"},
)

assert response.status_code == 401
assert response.json().get("detail") == "Could not validate credentials"


@pytest.mark.anyio
@pytest.mark.parametrize(
"query_params",
[
{}, # Missing parameters
{"course_id": 1234}, # Wrong "course_id" parameter data type
{
"course_id": "course_id_101",
"until": "today",
Expand All @@ -66,76 +91,106 @@ async def test_api_sliding_window_with_valid_params(
],
)
async def test_api_cohort_with_invalid_params(
query_params, http_client: httpx.AsyncClient
query_params, http_client: httpx.AsyncClient, auth_headers: dict
):
"""Test '/cohort' indicator with invalid query parameters returns 422 HTTP code."""
response = await http_client.get("/api/v1/tdbp/cohort", params=query_params)
"""Test `/cohort` endpoint with invalid query parameters returns 422 HTTP code."""
response = await http_client.get(
"/api/v1/tdbp/cohort",
params=query_params,
headers=auth_headers,
)

assert response.status_code == 422


@pytest.mark.anyio
async def test_api_cohort_with_valid_params(
http_client: httpx.AsyncClient, db_session, sliding_window_fake_dataset
http_client: httpx.AsyncClient,
auth_headers: dict,
db_session,
sliding_window_fake_dataset,
):
"""Test '/window' indicator with query parameters returns correct output."""
"""Test `/window` endpoint with query parameters returns correct output."""
course_id = "https://fake-lms.com/course/tdbp_101"
date_until = datetime.now().date()

response = await http_client.get(
"/api/v1/tdbp/cohort", params={"course_id": course_id, "until": date_until}
"/api/v1/tdbp/cohort",
params={"course_id": course_id, "until": date_until},
headers=auth_headers,
)

assert response.status_code == 200

assert len(response.json()) > 1


@pytest.mark.anyio
async def test_api_cohort_with_invalid_auth_headers(
http_client: httpx.AsyncClient,
):
"""Test `/cohort` endpoint with an invalid `auth_headers`."""
course_id = "https://fake-lms.com/course/tdbp_101"
date_until = datetime.now().date()
response = await http_client.get(
"/api/v1/tdbp/cohort",
params={"course_id": course_id, "until": date_until},
headers={"Authorization": "Bearer Wrong_Token"},
)

assert response.status_code == 401
assert response.json().get("detail") == "Could not validate credentials"


@pytest.mark.anyio
@pytest.mark.parametrize(
"query_params",
[
{}, # Missing parameters
{"course_id": 1234}, # Wrong "course_id" parameter data type
{
"course_id": "course_id_101",
"until": "today",
}, # Wrong "until" parameter data type
{
"course_id": "course_id_101",
"student_id": 12345,
}, # Wrong "student_id" parameter data type
{
"course_id": "course_id_101",
"totals": 1,
"totals": 2,
}, # Wrong "totals" parameter data type
{
"course_id": "course_id_101",
"average": 1,
"average": 2,
}, # Wrong "average" parameter data type
{"until": date.today()}, # Missing required "course_id" parameter
{"foo": "fake"}, # Unsupported parameter
],
)
async def test_api_scores_with_invalid_params(
query_params, http_client: httpx.AsyncClient
query_params, http_client: httpx.AsyncClient, auth_headers
):
"""Test '/scores' indicator with invalid query parameters returns 422 HTTP code."""
response = await http_client.get("/api/v1/tdbp/scores", params=query_params)
"""Test `/scores` endpoint with invalid query parameters returns 422 HTTP code."""
response = await http_client.get(
"/api/v1/tdbp/scores",
params=query_params,
headers=auth_headers,
)

assert response.status_code == 422


@pytest.mark.anyio
async def test_api_scores_with_valid_params(
http_client: httpx.AsyncClient, db_session, sliding_window_fake_dataset
http_client: httpx.AsyncClient,
auth_headers,
db_session,
sliding_window_fake_dataset,
):
"""Test '/scores' indicator with query parameters returns correct output."""
"""Test `/scores` endpoint with query parameters returns correct output."""
course_id = "https://fake-lms.com/course/tdbp_101"
date_until = datetime.now().date()

response = await http_client.get(
"/api/v1/tdbp/scores", params={"course_id": course_id, "until": date_until}
"/api/v1/tdbp/scores",
params={"course_id": course_id, "until": date_until},
headers=auth_headers,
)

assert response.status_code == 200
Expand All @@ -146,47 +201,68 @@ async def test_api_scores_with_valid_params(
pytest.fail(f"Scores indicator is invalid: {err}")


@pytest.mark.anyio
async def test_api_scores_with_invalid_auth_headers(
http_client: httpx.AsyncClient,
):
"""Test `/scores` endpoint with an invalid `auth_headers`."""
course_id = "https://fake-lms.com/course/tdbp_101"
date_until = datetime.now().date()
response = await http_client.get(
"/api/v1/tdbp/scores",
params={"course_id": course_id, "until": date_until},
headers={"Authorization": "Bearer Wrong_Token"},
)

assert response.status_code == 401
assert response.json().get("detail") == "Could not validate credentials"


@pytest.mark.anyio
@pytest.mark.parametrize(
"query_params",
[
{}, # Missing parameters
{"course_id": 1234}, # Wrong "course_id" parameter data type
{
"course_id": "course_id_101",
"until": "today",
}, # Wrong "until" parameter data type
{
"course_id": "course_id_101",
"student_id": 12345,
}, # Wrong "student_id" parameter data type
{
"course_id": "course_id_101",
"average": 1,
"average": 2,
}, # Wrong "average" parameter data type
{"until": date.today()}, # Missing required "course_id" parameter
{"foo": "fake"}, # Unsupported parameter
],
)
async def test_api_grades_with_invalid_params(
query_params, http_client: httpx.AsyncClient
query_params, http_client: httpx.AsyncClient, auth_headers: dict
):
"""Test '/grades' indicator with invalid query parameters returns 422 HTTP code."""
response = await http_client.get("/api/v1/tdbp/grades", params=query_params)
"""Test `/grades` endpoint with invalid query parameters returns 422 HTTP code."""
response = await http_client.get(
"/api/v1/tdbp/grades",
params=query_params,
headers=auth_headers,
)

assert response.status_code == 422


@pytest.mark.anyio
async def test_api_grades_with_valid_params(
http_client: httpx.AsyncClient, db_session, sliding_window_fake_dataset
http_client: httpx.AsyncClient,
auth_headers,
db_session,
sliding_window_fake_dataset,
):
"""Test '/grades' indicator with query parameters returns correct output."""
"""Test `/grades` endpoint with query parameters returns correct output."""
course_id = "https://fake-lms.com/course/tdbp_101"
date_until = datetime.now().date()

response = await http_client.get(
"/api/v1/tdbp/grades", params={"course_id": course_id, "until": date_until}
"/api/v1/tdbp/grades",
params={"course_id": course_id, "until": date_until},
headers=auth_headers,
)

assert response.status_code == 200
Expand All @@ -195,3 +271,20 @@ async def test_api_grades_with_valid_params(
Grades.parse_obj(response.json())
except ValidationError as err:
pytest.fail(f"Grades indicator is invalid: {err}")


@pytest.mark.anyio
async def test_api_grades_with_invalid_auth_headers(
http_client: httpx.AsyncClient,
):
"""Test `/grades` endpoint with an invalid `auth_headers`."""
course_id = "https://fake-lms.com/course/tdbp_101"
date_until = datetime.now().date()
response = await http_client.get(
"/api/v1/tdbp/grades",
params={"course_id": course_id, "until": date_until},
headers={"Authorization": "Bearer Wrong_Token"},
)

assert response.status_code == 401
assert response.json().get("detail") == "Could not validate credentials"
Loading

0 comments on commit 40481b8

Please sign in to comment.