From fe0f229223bc8193d39b97fdb14a18e68bfde58b Mon Sep 17 00:00:00 2001 From: Abdulrahman Abuzeid Date: Tue, 4 Aug 2026 22:00:05 +0200 Subject: [PATCH] Add API key authentication --- .env.example | 3 +- .github/workflows/ci.yml | 1 + README.md | 355 ++++++++++++++++++++------- app/main.py | 16 +- app/security.py | 33 +++ compose.yaml | 1 + tests/integration/test_localstack.py | 9 +- tests/test_artifacts.py | 58 ++++- 8 files changed, 383 insertions(+), 93 deletions(-) create mode 100644 app/security.py diff --git a/.env.example b/.env.example index bb711e4..5505cf3 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ -LOCALSTACK_AUTH_TOKEN=replace_with_your_localstack_token \ No newline at end of file +LOCALSTACK_AUTH_TOKEN=replace_with_your_localstack_token +API_KEY=replace-with-a-secure-random-value \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6748ed1..64e56ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,7 @@ jobs: AWS_SECRET_ACCESS_KEY: test AWS_DEFAULT_REGION: us-east-1 AWS_REGION: us-east-1 + API_KEY: ${{ secrets.API_KEY }} RUN_INTEGRATION_TESTS: "1" TF_IN_AUTOMATION: "true" diff --git a/README.md b/README.md index 601081e..9daef87 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,31 @@ [![Continuous Integration](https://github.com/abdulrahmancoding/secure-api-devsecops/actions/workflows/ci.yml/badge.svg)](https://github.com/abdulrahmancoding/secure-api-devsecops/actions/workflows/ci.yml) -A security focused DevSecOps project demonstrating how to build, test, containerize, scan, and provision cloud infrastructure for a Python API. +A security-focused DevSecOps project demonstrating how to build, test, containerize, scan, and provision cloud infrastructure for a Python API. -The project combines FastAPI, Docker, Terraform, GitHub Actions, and AWS compatible services running locally through LocalStack. It applies automated security checks throughout the development workflow without requiring a billing enabled AWS account. +The project combines FastAPI, Docker, Terraform, GitHub Actions, and AWS-compatible services running locally through LocalStack. It applies automated security checks throughout the development workflow without requiring a billing-enabled AWS account. ## Project Overview -The API stores text artifacts in an S3 compatible bucket protected by a customer managed KMS encryption key. Terraform defines the infrastructure and security controls as code, while GitHub Actions automatically tests and scans every change pushed to the repository. +The API accepts text artifacts and stores them in an S3-compatible bucket protected by a customer-managed KMS encryption key. + +Protected API endpoints require a valid API key supplied through the `X-API-Key` request header. Terraform defines the infrastructure and its security controls as code, while GitHub Actions automatically tests and scans every change. The project demonstrates: - Containerized API development with Docker +- API-key authentication +- Secure secret management using environment variables and GitHub Actions secrets - AWS infrastructure provisioning with Terraform - Encrypted object storage using S3 and KMS -- Least privilege IAM permissions -- Automated testing with Pytest -- Dependency vulnerability scanning with pip audit +- Least-privilege IAM permissions +- Automated unit testing with Pytest +- Automated LocalStack integration testing +- Dependency vulnerability scanning with pip-audit - Python static security analysis with Bandit - Container and Terraform scanning with Trivy - Continuous integration with GitHub Actions +- Pull-request-based development ## Architecture @@ -29,82 +35,132 @@ flowchart LR Developer[Developer] -->|Push code| GitHub[GitHub Repository] GitHub --> CI[GitHub Actions CI] - CI --> Tests[Pytest] + CI --> Tests[Pytest Unit Tests] CI --> CodeScan[Bandit and pip-audit] - CI --> ImageScan[Docker build and Trivy scan] - CI --> IaCScan[Terraform validation and Trivy scan] + CI --> ImageScan[Docker Build and Trivy Scan] + CI --> IaCScan[Terraform Validation and Trivy Scan] + CI --> IntegrationTest[LocalStack Integration Test] - Client[API Client] -->|HTTP request| API[FastAPI Container] - API -->|Boto3| S3[LocalStack S3] - S3 -->|Encrypt objects| KMS[Customer managed KMS Key] + Client[API Client] -->|X-API-Key Header| API[FastAPI Container] + API --> Auth[API-Key Validation] + Auth -->|Authorized Request| Boto3[Boto3 Client] + Boto3 --> S3[LocalStack S3] + S3 -->|Encrypt Objects| KMS[Customer-Managed KMS Key] Terraform[Terraform] --> S3 Terraform --> KMS - Terraform --> IAM[Least privilege IAM Role] + Terraform --> IAM[Least-Privilege IAM Role] ``` ### Request Flow -1. A client sends an artifact to the FastAPI application. -2. The API validates the artifact name and content. -3. Boto3 sends the artifact to the local S3-compatible service. -4. S3 encrypts the stored object using the customer managed KMS key. -5. The API can list stored artifacts without receiving unnecessary administrative permissions. +1. A client sends a request to the FastAPI application. +2. The API checks the `X-API-Key` request header. +3. Requests with missing or incorrect credentials are rejected. +4. The API validates the artifact name and content. +5. Boto3 sends the artifact to the local S3-compatible service. +6. S3 encrypts the stored object using the customer-managed KMS key. +7. Authorized clients can list the stored artifact names. ## Security Controls | Area | Control | Implementation | |---|---|---| -| Data protection | Encryption at rest | S3 objects use a customer managed KMS key | +| API security | API-key authentication | Artifact endpoints require a valid `X-API-Key` header | +| Secret management | Environment isolation | Local secrets are stored in an ignored `.env` file | +| CI secret management | Encrypted repository secrets | API and LocalStack credentials are stored as GitHub Actions secrets | +| Credential comparison | Timing-safe comparison | API keys are checked using `secrets.compare_digest` | +| Data protection | Encryption at rest | S3 objects use a customer-managed KMS key | | Key management | Automatic key rotation | KMS key rotation is enabled in Terraform | -| Storage security | Public access prevention | All four S3 public access block settings are enabled | -| Transport security | HTTPS only access | The bucket policy denies requests using insecure transport | +| Storage security | Public access prevention | All four S3 public-access-block settings are enabled | +| Transport security | HTTPS-only access | The bucket policy denies requests using insecure transport | | Data recovery | Object versioning | S3 bucket versioning preserves previous object versions | -| Access control | Least privilege | The API role can only list the designated bucket, upload objects, and use its specific KMS key | +| Access control | Least privilege | The API role is limited to the required S3 and KMS operations | | Input security | Request validation | Artifact names and content sizes are restricted using Pydantic | -| Application security | Safe error handling | Storage failures return a controlled HTTP 503 response | -| Container security | Non root execution | The API container runs as an unprivileged application user | +| Application security | Safe error handling | Storage failures return a controlled HTTP `503` response | +| Container security | Non-root execution | The API container runs as an unprivileged application user | | Network exposure | Localhost binding | API and LocalStack ports are bound to `127.0.0.1` | -| CI permissions | Read only repository access | GitHub Actions receives only `contents: read` permission | -| Dependency security | Vulnerability auditing | pip audit checks Python dependencies for known vulnerabilities | +| CI permissions | Read-only repository access | GitHub Actions receives only `contents: read` permission | +| Dependency security | Vulnerability auditing | pip-audit checks production dependencies for known vulnerabilities | | Source security | Static analysis | Bandit scans Python source code for insecure patterns | -| Image security | Container scanning | Trivy blocks builds containing high or critical image vulnerabilities | +| Image security | Container scanning | Trivy blocks builds containing high or critical vulnerabilities | | Infrastructure security | IaC scanning | Terraform is formatted, validated, and scanned by Trivy | ## Continuous Integration Pipeline The GitHub Actions workflow runs on pushes and pull requests targeting the `main` branch. +The workflow is separated into three jobs. + ### Application Pipeline 1. Check out the repository without preserving Git credentials. -2. Install the pinned Python dependencies. -3. Audit production dependencies with pip-audit. -4. scan Python source code with Bandit; -5. run the automated Pytest suite; -6. build the Docker image; -7. scan the completed image with Trivy. +2. Set up the required Python version. +3. Install the pinned Python dependencies. +4. Audit production dependencies with pip-audit. +5. Scan Python source code with Bandit. +6. Run the automated Pytest suite. +7. Build the Docker image. +8. Scan the completed image with Trivy. ### Infrastructure Pipeline -1. Install the required Terraform version. -2. verify Terraform formatting; -3. initialize Terraform without a remote backend; -4. validate the Terraform configuration; -5. scan the infrastructure code for high and critical misconfigurations. +1. Check out the repository. +2. Install the required Terraform version. +3. Verify Terraform formatting. +4. Initialize Terraform without a remote backend. +5. Validate the Terraform configuration. +6. Scan the infrastructure code for high and critical misconfigurations. + +### Integration Test Pipeline + +The integration job starts only after the application and infrastructure jobs pass. + +It then: -A failed test, vulnerability check, or infrastructure scan stops the workflow and prevents the issue from being silently accepted. +1. Starts a temporary LocalStack environment. +2. Initializes and applies the Terraform configuration. +3. Creates the local S3, KMS, and IAM resources. +4. Sends an authenticated request through FastAPI. +5. Stores an artifact through Boto3 in LocalStack S3. +6. Retrieves the stored object. +7. Verifies its content. +8. Confirms that KMS encryption was applied. +9. Deletes the temporary test object. + +A failed test, vulnerability check, container scan, infrastructure scan, or integration test stops the workflow. + +## API Authentication + +The artifact endpoints require an API key. + +Clients must send the key in this HTTP header: + +```text +X-API-Key: your-api-key +``` + +Authentication behavior: + +- Missing API key: `401 Unauthorized` +- Incorrect API key: `401 Unauthorized` +- Authentication not configured on the server: `503 Service Unavailable` +- Correct API key: the request continues normally + +The `/health` endpoint remains public so monitoring systems can check whether the service is running. + +The API key must never be committed to Git or included in screenshots, logs, or documentation. ## API Endpoints -| Method | Endpoint | Purpose | Successful response | -|---|---|---|---| -| `GET` | `/health` | Check whether the API is running | `200 OK` | -| `POST` | `/artifacts` | Validate and store a text artifact | `201 Created` | -| `GET` | `/artifacts` | List the stored artifact names | `200 OK` | -| `GET` | `/docs` | Open the interactive API documentation | `200 OK` | +| Method | Endpoint | Authentication | Purpose | Successful response | +|---|---|---|---|---| +| `GET` | `/health` | Public | Check whether the API is running | `200 OK` | +| `POST` | `/artifacts` | API key required | Validate and store a text artifact | `201 Created` | +| `GET` | `/artifacts` | API key required | List stored artifact names | `200 OK` | +| `GET` | `/docs` | Public | Open the interactive API documentation | `200 OK` | -Example upload request: +### Example Upload Body ```json { @@ -113,25 +169,84 @@ Example upload request: } ``` +### Example Authenticated Upload + +```bash +curl --request POST \ + --url http://localhost:8000/artifacts \ + --header "Content-Type: application/json" \ + --header "X-API-Key: $API_KEY" \ + --data '{ + "name": "security-report.txt", + "content": "No critical vulnerabilities found." + }' +``` + +Expected response: + +```json +{ + "name": "security-report.txt", + "status": "stored" +} +``` + +### Example Authenticated Artifact Listing + +```bash +curl \ + --header "X-API-Key: $API_KEY" \ + http://localhost:8000/artifacts +``` + ## Testing Strategy -The automated test suite contains five tests covering: +The automated unit test suite contains eight tests covering: - API health checking - Successful artifact uploads - Artifact listing - Rejection of unsafe artifact names - Controlled behavior when storage is unavailable +- Rejection of missing API keys +- Rejection of incorrect API keys +- Controlled behavior when authentication is not configured -Unit tests replace the real storage connection with temporary fake functions. This keeps the CI pipeline fast and allows application behavior to be tested without starting LocalStack. +Unit tests replace the real storage connection with temporary fake functions. This keeps the basic test suite fast and allows application behavior to be tested without starting LocalStack. -A separate integration test was performed manually to verify the complete flow from FastAPI through Boto3 to LocalStack S3. The uploaded object was then inspected to confirm that it used the customer-managed KMS encryption key. +A separate integration test verifies the complete flow: -Run the automated tests locally with: +```text +FastAPI → API-key authentication → Boto3 → LocalStack S3 → KMS encryption +``` + +Unlike the unit tests, the integration test uses real local infrastructure created by Terraform. It verifies that: + +- The authenticated upload request succeeds +- The artifact appears in the S3 bucket +- The stored content matches the uploaded content +- The object uses KMS encryption +- The temporary test object is deleted afterward + +Run the unit tests locally with: ```bash python -m pytest -v ``` + +Run the LocalStack integration test with: + +```bash +RUN_INTEGRATION_TESTS=1 \ +AWS_ACCESS_KEY_ID=test \ +AWS_SECRET_ACCESS_KEY=test \ +AWS_DEFAULT_REGION=us-east-1 \ +API_KEY="$API_KEY" \ +python -m pytest tests/integration/test_localstack.py -v +``` + +LocalStack must be running and the Terraform infrastructure must be applied before running the integration test. + ## Running the Project Locally ### Prerequisites @@ -139,6 +254,7 @@ python -m pytest -v Install the following tools before starting: - Git +- Python - Docker Desktop with Docker Compose - Terraform `1.15.8` - A LocalStack authentication token @@ -160,9 +276,22 @@ cd secure-api-devsecops cp .env.example .env ``` -Add your LocalStack token to `.env`. Never commit this file or share its contents. +3. Open `.env` and configure: -3. Start the local AWS environment: +```text +LOCALSTACK_AUTH_TOKEN=your-localstack-token +API_KEY=your-secure-random-api-key +``` + +Generate a secure API key with: + +```bash +python -c "import secrets; print(secrets.token_urlsafe(32))" +``` + +Never commit `.env`, display its contents publicly, or include its secrets in screenshots. + +4. Start LocalStack: ```bash docker compose up --detach localstack @@ -171,7 +300,7 @@ docker compose ps Wait until LocalStack reports a healthy status. -4. Initialize and apply the Terraform infrastructure: +5. Initialize and apply the Terraform infrastructure: ```bash terraform -chdir=infrastructure init @@ -180,13 +309,14 @@ terraform -chdir=infrastructure apply Review the plan and enter `yes` when prompted. -5. Build and start the API: +6. Build and start the API: ```bash docker compose up --detach --build api +docker compose ps ``` -6. Verify the service: +7. Verify the public health endpoint: ```bash curl http://localhost:8000/health @@ -198,12 +328,32 @@ Expected response: {"status":"healthy"} ``` -Open the interactive API documentation at: +8. Load the environment variables into the current terminal: + +```bash +set -a +source .env +set +a +``` + +9. Verify an authenticated endpoint: + +```bash +curl \ + --header "X-API-Key: $API_KEY" \ + http://localhost:8000/artifacts +``` + +10. Open the interactive API documentation: ```text http://localhost:8000/docs ``` +Click **Authorize**, enter the API key, and then test the protected endpoints. + +Do not share screenshots containing the generated API key or Swagger curl commands containing the `X-API-Key` header. + ### Stop the Environment ```bash @@ -212,11 +362,27 @@ docker compose down The AWS resources exist only inside the local environment and do not create charges in a real AWS account. +## Local Security Checks + +Run the same major checks locally before committing changes: + +```bash +python -m pytest -v +bandit --recursive app +python -m pip_audit --requirement requirements.txt +terraform -chdir=infrastructure fmt -check +terraform -chdir=infrastructure validate +git diff --check +``` + +These commands verify application behavior, scan the source code and dependencies, validate the infrastructure, and detect formatting problems. + ## Technology Stack | Category | Technology | |---|---| | API | Python, FastAPI, Pydantic | +| Authentication | API key through the `X-API-Key` header | | AWS integration | Boto3 | | Containers | Docker, Docker Compose | | Local cloud environment | LocalStack | @@ -236,11 +402,14 @@ secure-api-devsecops/ │ └── ci.yml ├── app/ │ ├── main.py +│ ├── security.py │ └── storage.py ├── infrastructure/ │ ├── .terraform.lock.hcl │ └── main.tf ├── tests/ +│ ├── integration/ +│ │ └── test_localstack.py │ ├── test_artifacts.py │ └── test_health.py ├── .dockerignore @@ -253,41 +422,63 @@ secure-api-devsecops/ └── README.md ``` -- `app/` contains the API and S3 integration code. -- `infrastructure/` contains the AWS compatible Terraform configuration. -- `tests/` contains the automated API tests. -- `.github/workflows/` contains the CI and security pipeline. -- `compose.yaml` connects the API to the local AWS environment. +- `app/main.py` defines the API endpoints and request validation. +- `app/security.py` validates API keys. +- `app/storage.py` connects the application to S3 through Boto3. +- `infrastructure/` contains the AWS-compatible Terraform configuration. +- `tests/` contains the unit and integration tests. +- `.github/workflows/ci.yml` defines the CI and security pipeline. +- `compose.yaml` connects the API container to LocalStack. +- `.env.example` documents the required environment variables without exposing real secrets. + +## Current Scope + +The project currently provides: -## Current Scope and Future Improvements +- A containerized FastAPI service +- Protected artifact-management endpoints +- API-key authentication +- Secure S3-compatible artifact storage +- Customer-managed KMS encryption +- Least-privilege IAM resources +- Terraform-based infrastructure provisioning +- Automated unit and integration testing +- Automated source, dependency, image, and infrastructure scanning +- A multi-job GitHub Actions pipeline +- Local development without a billing-enabled AWS account -This project intentionally uses LocalStack so the complete workflow can run without a billing-enabled AWS account. The infrastructure follows AWS-compatible patterns, but it has not yet been deployed to a production AWS environment. +The infrastructure follows AWS-compatible patterns but has not been deployed to a production AWS environment. + +## Future Improvements Potential future improvements include: -- Deploying the API to Amazon ECS or another managed container platform -- Using GitHub OpenID Connect instead of long-lived deployment credentials -- Adding API authentication and user specific authorization -- Adding structured logs, metrics, and alerts -- Automating the LocalStack integration test inside CI +- Adding object download and deletion endpoints +- Replacing the single API key with user-specific authentication and authorization +- Adding API-key rotation and expiration +- Adding rate limiting to protect the API from abuse +- Adding structured security logs and request identifiers +- Adding monitoring, metrics, dashboards, and alerts +- Deploying the container to Amazon ECS or another managed platform +- Using GitHub OpenID Connect for secure AWS deployment authentication - Storing Terraform state in a secured remote backend -- Adding object download and deletion endpoints with carefully scoped permissions +- Adding separate development, staging, and production environments +- Adding automated API documentation and release versioning +- Adding policy-as-code checks with tools such as Checkov or Open Policy Agent ## Learning Outcomes Building this project provided practical experience with: - Designing a complete DevSecOps workflow -- Connecting a containerized API to AWS compatible services -- Managing infrastructure and security controls using Terraform -- Applying encryption, access control, and least privilege principles -- Separating fast unit tests from full integration testing -- Investigating and fixing failed CI security checks -- Protecting secrets and preventing them from entering version control -- Documenting technical architecture for other developers - -## Author - -**Abdulrahman Abuzeid** - -GitHub: [abdulrahmancoding](https://github.com/abdulrahmancoding) \ No newline at end of file +- Building and containerizing a Python API +- Protecting endpoints with API-key authentication +- Managing secrets safely in local and CI environments +- Provisioning AWS-compatible resources using Terraform +- Applying S3, KMS, IAM, and network security controls +- Writing unit and end-to-end integration tests +- Testing real interactions between FastAPI, Boto3, S3, and KMS +- Scanning source code, dependencies, containers, and infrastructure +- Building multi-job GitHub Actions workflows +- Debugging Docker, WSL, YAML, Terraform, LocalStack, and CI failures +- Using feature branches and pull requests to deliver changes safely \ No newline at end of file diff --git a/app/main.py b/app/main.py index d5e3c10..689a7ce 100644 --- a/app/main.py +++ b/app/main.py @@ -1,14 +1,15 @@ from botocore.exceptions import BotoCoreError, ClientError -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Security from pydantic import BaseModel, Field +from app.security import require_api_key from app.storage import list_artifacts, upload_artifact app = FastAPI( title="Secure API", description="A containerized API for learning DevSecOps practices.", - version="0.2.0", + version="0.3.0", ) @@ -26,7 +27,11 @@ def health_check() -> dict[str, str]: return {"status": "healthy"} -@app.post("/artifacts", status_code=201) +@app.post( + "/artifacts", + status_code=201, + dependencies=[Security(require_api_key)], +) def create_artifact(artifact: ArtifactCreate) -> dict[str, str]: try: upload_artifact(artifact.name, artifact.content) @@ -42,7 +47,10 @@ def create_artifact(artifact: ArtifactCreate) -> dict[str, str]: } -@app.get("/artifacts") +@app.get( + "/artifacts", + dependencies=[Security(require_api_key)], +) def get_artifacts() -> dict[str, list[str]]: try: names = list_artifacts() diff --git a/app/security.py b/app/security.py new file mode 100644 index 0000000..d061295 --- /dev/null +++ b/app/security.py @@ -0,0 +1,33 @@ +import os +from secrets import compare_digest +from typing import Annotated + +from fastapi import HTTPException, Security, status +from fastapi.security import APIKeyHeader + + +api_key_header = APIKeyHeader( + name="X-API-Key", + auto_error=False, +) + + +def require_api_key( + provided_api_key: Annotated[str | None, Security(api_key_header)], +) -> None: + expected_api_key = os.getenv("API_KEY") + + if not expected_api_key: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="API authentication is not configured", + ) + + if provided_api_key is None or not compare_digest( + provided_api_key, + expected_api_key, + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or missing API key", + ) \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 349eba1..e692f5c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -20,6 +20,7 @@ services: AWS_REGION: us-east-1 AWS_ENDPOINT_URL: http://localstack:4566 S3_BUCKET_NAME: secure-api-artifacts-local + API_KEY: "${API_KEY:?API_KEY must be configured in .env}" depends_on: localstack: condition: service_healthy \ No newline at end of file diff --git a/tests/integration/test_localstack.py b/tests/integration/test_localstack.py index a53c4d5..10cf83a 100644 --- a/tests/integration/test_localstack.py +++ b/tests/integration/test_localstack.py @@ -18,12 +18,16 @@ def test_artifact_storage_with_localstack() -> None: artifact_name = f"integration-{uuid4().hex}.txt" artifact_content = "Integration test successfully reached encrypted S3." + auth_headers = { + "X-API-Key": os.environ["API_KEY"], + } s3_client = get_s3_client() try: upload_response = client.post( "/artifacts", + headers=auth_headers, json={ "name": artifact_name, "content": artifact_content, @@ -36,7 +40,10 @@ def test_artifact_storage_with_localstack() -> None: "status": "stored", } - list_response = client.get("/artifacts") + list_response = client.get( + "/artifacts", + headers=auth_headers, + ) assert list_response.status_code == 200 assert artifact_name in list_response.json()["artifacts"] diff --git a/tests/test_artifacts.py b/tests/test_artifacts.py index 5a43270..4cebba9 100644 --- a/tests/test_artifacts.py +++ b/tests/test_artifacts.py @@ -1,13 +1,22 @@ +import pytest from botocore.exceptions import EndpointConnectionError from fastapi.testclient import TestClient from app.main import app +TEST_API_KEY = "unit-test-api-key" +AUTH_HEADERS = {"X-API-Key": TEST_API_KEY} + client = TestClient(app) -def test_create_artifact(monkeypatch) -> None: +@pytest.fixture(autouse=True) +def configure_test_api_key(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("API_KEY", TEST_API_KEY) + + +def test_create_artifact(monkeypatch: pytest.MonkeyPatch) -> None: stored: dict[str, str] = {} def fake_upload(name: str, content: str) -> None: @@ -17,6 +26,7 @@ def fake_upload(name: str, content: str) -> None: response = client.post( "/artifacts", + headers=AUTH_HEADERS, json={ "name": "security-report.txt", "content": "No critical vulnerabilities found.", @@ -31,13 +41,16 @@ def fake_upload(name: str, content: str) -> None: assert stored["security-report.txt"] == "No critical vulnerabilities found." -def test_list_artifacts(monkeypatch) -> None: +def test_list_artifacts(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( "app.main.list_artifacts", lambda: ["security-report.txt", "scan-results.txt"], ) - response = client.get("/artifacts") + response = client.get( + "/artifacts", + headers=AUTH_HEADERS, + ) assert response.status_code == 200 assert response.json() == { @@ -48,6 +61,7 @@ def test_list_artifacts(monkeypatch) -> None: def test_reject_invalid_artifact_name() -> None: response = client.post( "/artifacts", + headers=AUTH_HEADERS, json={ "name": "../secret.txt", "content": "invalid", @@ -57,7 +71,7 @@ def test_reject_invalid_artifact_name() -> None: assert response.status_code == 422 -def test_storage_unavailable(monkeypatch) -> None: +def test_storage_unavailable(monkeypatch: pytest.MonkeyPatch) -> None: def unavailable(name: str, content: str) -> None: raise EndpointConnectionError(endpoint_url="http://localhost:4566") @@ -65,6 +79,7 @@ def unavailable(name: str, content: str) -> None: response = client.post( "/artifacts", + headers=AUTH_HEADERS, json={ "name": "report.txt", "content": "test", @@ -72,4 +87,37 @@ def unavailable(name: str, content: str) -> None: ) assert response.status_code == 503 - assert response.json() == {"detail": "Storage service unavailable"} \ No newline at end of file + assert response.json() == {"detail": "Storage service unavailable"} + + +def test_reject_missing_api_key() -> None: + response = client.get("/artifacts") + + assert response.status_code == 401 + assert response.json() == {"detail": "Invalid or missing API key"} + + +def test_reject_incorrect_api_key() -> None: + response = client.get( + "/artifacts", + headers={"X-API-Key": "incorrect-key"}, + ) + + assert response.status_code == 401 + assert response.json() == {"detail": "Invalid or missing API key"} + + +def test_authentication_not_configured( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("API_KEY", raising=False) + + response = client.get( + "/artifacts", + headers=AUTH_HEADERS, + ) + + assert response.status_code == 503 + assert response.json() == { + "detail": "API authentication is not configured" + } \ No newline at end of file