Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add maintenance and scheduled endpoints #88 #96

Merged
merged 32 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
70ca806
added maintenance route #88
MatteoGuarnaccia5 Jun 17, 2024
066f784
added scheduled maintenance endpoint #88
MatteoGuarnaccia5 Jun 17, 2024
8123ced
changed prefix of maintenance router to match issue #88
MatteoGuarnaccia5 Jun 17, 2024
306da3a
changed maintenance state file to be json #88
MatteoGuarnaccia5 Jun 18, 2024
ced71f7
implemented error handling for incorrectly formated file #88
MatteoGuarnaccia5 Jun 18, 2024
6bce2e4
added returns and raises to functions docstrings #88
MatteoGuarnaccia5 Jun 18, 2024
c0c5290
added unit tests for maintenance #88
MatteoGuarnaccia5 Jun 18, 2024
2232fe0
added `from exc` in maintenance router, which fixes unit tests #88
MatteoGuarnaccia5 Jun 18, 2024
5b91ba0
added exc value when raising error in maintenance class #88
MatteoGuarnaccia5 Jun 18, 2024
972d3c9
removed unused imports, fixed docstring linting, fixed exception rais…
MatteoGuarnaccia5 Jun 18, 2024
54ef3c3
updated read me #88
MatteoGuarnaccia5 Jun 20, 2024
cb11de8
moved maintenance folder to root level #88
MatteoGuarnaccia5 Jun 20, 2024
2d48798
added file paths to docker compose #88
MatteoGuarnaccia5 Jun 20, 2024
00ebf99
added more exceptions, and refactored path to open files #88
MatteoGuarnaccia5 Jun 20, 2024
8a2a794
fixed linting #88
MatteoGuarnaccia5 Jun 20, 2024
973d73e
changed default values in maintenance json files #88
MatteoGuarnaccia5 Jun 24, 2024
70fa839
refactored ReadMe to include allowed severities and how to add/remove…
MatteoGuarnaccia5 Jun 24, 2024
1948249
refactored text in docstrings, function names, and exceptions #88
MatteoGuarnaccia5 Jun 24, 2024
8cc1673
changed path in maintenance core to value in `APIConfig` class #88
MatteoGuarnaccia5 Jun 24, 2024
909b6f9
removed `severity` from scheduled maintenance #88
MatteoGuarnaccia5 Jun 24, 2024
1334f7e
fixed unit test
MatteoGuarnaccia5 Jun 24, 2024
d0e41a9
fixed linting #88
MatteoGuarnaccia5 Jun 24, 2024
54a4e7b
added new env variables to pytest.ini
MatteoGuarnaccia5 Jun 24, 2024
4da1746
refactored exception catching to include all possible exceptions #88
MatteoGuarnaccia5 Jun 24, 2024
d752ce0
fixed typo
MatteoGuarnaccia5 Jun 25, 2024
e21e09a
refactored formatting #88
MatteoGuarnaccia5 Jul 1, 2024
c98e731
refactored exceptions in maintenance class #88
MatteoGuarnaccia5 Jul 1, 2024
9b1297e
updated assertions in tests to match new exceptions #88
MatteoGuarnaccia5 Jul 1, 2024
d5f29fa
fixed typo
MatteoGuarnaccia5 Jul 2, 2024
644b52c
change assertion to use is
MatteoGuarnaccia5 Jul 2, 2024
c3a8b58
run black formatter
MatteoGuarnaccia5 Jul 2, 2024
860cd8b
removed duplicate text
MatteoGuarnaccia5 Jul 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 61 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,41 @@ This is a Python microservice created using FastAPI that provides user authentic
a JSON Web Token (JWT).

## How to Run

This microservice requires an LDAP server to run against.

### Prerequisites

- Docker and Docker Compose installed (if you want to run the microservice inside Docker)
- Python 3.12 installed on your machine (if you are not using Docker)
- LDAP server to connect to
- CA certificate PEM file containing all the trusted CA certificates (if LDAP certificate validation is enabled which is
strongly recommended to be in production)
- Private and public key pair (must be OpenSSH encoded) for encrypting and decrypting the JWTs
- A list of active usernames, defining who can use this service
- A list of active usernames, defining who can use this service
- This repository cloned

### Docker Setup

Ensure that Docker is installed and running on your machine before proceeding.

1. Create a `.env` file alongside the `.env.example` file. Use the example file as a reference and modify the values
accordingly.

```bash
cp ldap_jwt_auth/.env.example ldap_jwt_auth/.env
```

2. Create a `logging.ini` file alongside the `logging.example.ini` file. Use the example file as a reference and modify
it accordingly:

```bash
cp ldap_jwt_auth/logging.example.ini ldap_jwt_auth/logging.ini
```

3. Navigate to the `keys` directory in the root of the project directory, and generate OpenSSH encoded private and
public key pair:

```bash
ssh-keygen -b 2048 -t rsa -f keys/jwt-key -q -N "" -C ""
```
Expand All @@ -47,6 +53,7 @@ Ensure that Docker is installed and running on your machine before proceeding.
```

#### Using `docker-compose.yml`

The easiest way to run the application with Docker for local development is using the `docker-compose.yml` file. It is
configured to start the application in a reload mode using the `Dockerfile`.

Expand All @@ -58,10 +65,12 @@ configured to start the application in a reload mode using the `Dockerfile`.
at http://localhost:8000/docs.

#### Using `Dockerfile`

Use the `Dockerfile` to run just the application itself in a container. Use this only for local development (not
production)!

1. Build an image using the `Dockerfile` from the root of the project directory:

```bash
docker build -f Dockerfile -t ldap_jwt_auth_api_image .
```
Expand All @@ -78,18 +87,21 @@ production)!
at http://localhost:8000/docs.

#### Using `Dockerfile.prod`

Use the `Dockerfile.prod` to run just the application itself in a container. This can be used for production.

1. Private keys are only readable by the owner. Given that the private key is generated on the host machine and the
container runs with a different user, it means that the key is not readable by the user in the container because the
ownership belongs to the user on the host. This can be solved by transferring the ownership to the user in the
container and setting the permissions.

```bash
sudo chown 500:500 keys/jwt-key
sudo chmod 0400 keys/jwt-key
```

2. Build an image using the `Dockerfile.prod` from the root of the project directory:

```bash
docker build -f Dockerfile.prod -t ldap_jwt_auth_api_image .
```
Expand All @@ -106,9 +118,11 @@ Use the `Dockerfile.prod` to run just the application itself in a container. Thi
at http://localhost:8000/docs.

### Local Setup

Ensure that Python is installed on your machine before proceeding.

1. Create a Python virtual environment and activate it in the root of the project directory:

```bash
python -m venv venv
source venv/bin/activate
Expand All @@ -123,18 +137,21 @@ Ensure that Python is installed on your machine before proceeding.
```
4. Create a `.env` file alongside the `.env.example` file. Use the example file as a reference and modify the values
accordingly.

```bash
cp ldap_jwt_auth/.env.example ldap_jwt_auth/.env
```

5. Create a `logging.ini` file alongside the `logging.example.ini` file. Use the example file as a reference and modify
it accordingly:

```bash
cp ldap_jwt_auth/logging.example.ini ldap_jwt_auth/logging.ini
```

6. Navigate to the `keys` directory in the root of the project directory, and generate OpenSSH encoded private and
public key pair:

```bash
ssh-keygen -b 2048 -t rsa -f keys/jwt-key -q -N "" -C ""
```
Expand All @@ -144,25 +161,30 @@ Ensure that Python is installed on your machine before proceeding.

8. Create a `active_usernames.txt` file alongside the `active_usernames.example.txt` file and add all the usernames that
can use this system. The usernames are the Federal IDs and each one should be stored on a separate line.

```bash
cp active_usernames.example.txt active_usernames.txt
```

9. Start the microservice using Uvicorn:

```bash
uvicorn ldap_jwt_auth.main:app --log-config ldap_jwt_auth/logging.ini --reload
```

The microservice should now be running locally at http://localhost:8000. The Swagger UI could be accessed
at http://localhost:8000/docs.

10. To run the unit tests, run:
```bash
pytest -c test/pytest.ini test/unit/
```

```bash
pytest -c test/pytest.ini test/unit/
```
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved

## Notes

### Application Configuration

The configuration for the application is handled
using [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). It allows for loading config
values from environment variables or the `.env` file. Please note that even when using the `.env` file, Pydantic will
Expand All @@ -172,7 +194,7 @@ values loaded from the `.env` file.
Listed below are the environment variables supported by the application.

| Environment Variable | Description | Mandatory | Default Value |
|-------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|-----------|-----------------------------------------------------------|
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------- |
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved
| `API__TITLE` | The title of the API which is added to the generated OpenAPI. | No | `LDAP-JWT Authentication Service API` |
| `API__DESCRIPTION` | The description of the API which is added to the generated OpenAPI. | No | `This is the API for the LDAP-JWT Authentication Service` |
| `API__ROOT_PATH` | (If using a proxy) The path prefix handled by a proxy that is not seen by the app. | No | ` ` |
Expand All @@ -184,6 +206,39 @@ Listed below are the environment variables supported by the application.
| `AUTHENTICATION__JWT_ALGORITHM` | The algorithm to use to decode the JWT access and refresh tokens. | Yes | |
| `AUTHENTICATION__ACCESS_TOKEN_VALIDITY_MINUTES` | Minutes after which the JWT access token expires. | Yes | |
| `AUTHENTICATION__REFRESH_TOKEN_VALIDITY_DAYS` | Days after which the JWT refresh token expires. | Yes | |
| `AUTHENTICATION__ACTIVE_USERNAMES_PATH` | The path to the `txt` file containing the active usernames and defining who can use this service. | Yes | |
| `AUTHENTICATION__ACTIVE_USERNAMES_PATH` | The path to the `txt` file containing the active usernames and defining who can use this service. | Yes |
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved
| `MAINTENANCE__MAINTENANCE_PATH` | The path to the `json` file containing the maintenance state. | Yes | |
| `MAINTENANCE__SCHEDULED_MAINTENANCE_PATH` | The path to the `json` file containing the scheduled maintenance state. | Yes |
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved
| `LDAP_SERVER__URL` | The URL to the LDAP server to connect to. | Yes | |
| `LDAP_SERVER__REALM` | The realm for the LDAP server. | Yes | |

### How to add or remove user from system

The `active_usernames.txt` file at the root of the project directory contains the Federal IDs of the users with access to the system. This means that you can add or
remove a user from the system by adding or removing their Federal ID in the `active_usernames.txt` file.
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved

**PLEASE NOTE** Changes made to the `active_usernames.txt` file using vim do not get synced because it changes the inode
index number of the file. A workaround is to create a new file using the `active_usernames.txt` file, apply your changes
in the new file, and then overwrite the `active_usernames.txt` file with the content of the new file, see below.
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved

```bash
cp active_usernames.txt new_active_usernames.txt
vim new_active_usernames.txt
cat new_active_usernames.txt > active_usernames.txt
rm new_active_usernames.txt
```

### How to update maintenance or scheduled maintenance state
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved

The `maintenance` folder at the root of the project directory contains two json files which return the appropiate state of the system. This means that you can edit the values in the files in accordance with the desired state of the system.

MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved
**_PLEASE NOTE_** Changes made to `maintenance.json` and `scheduled_maintenance.json` file using vim do not get synced because it changes the inode
index number of the file. A workaround is to create a new file using the `maintenance.json` or `scheduled_maintenance.json` file, apply your changes
in the new file, and then overwrite the `maintenance.json` / `scheduled_maintenance.json` file with the content of the new file, see below an example for `maintenance.json` file.
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved

```bash
cp maintenance/maintenance.json new_maintenance.json
vim new_maintenance.json
cat new_maintenance.json > maintenance/maintenance.json
rm new_maintenance.json
```
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ services:
- ./keys:/ldap-jwt-auth-run/keys
- ./ldap_server_certs/cacert.pem:/ldap-jwt-auth-run/ldap_server_certs/cacert.pem
- ./active_usernames.txt:/ldap-jwt-auth-run/active_usernames.txt
- ./maintenance/maintenance.json:/ldap-jwt-auth-run/maintenance/maintenance.json
- ./maintenance/scheduled_maintenance.json:/ldap-jwt-auth-run/maintenance/scheduled_maintenance.json
ports:
- 8000:8000
restart: on-failure
2 changes: 2 additions & 0 deletions ldap_jwt_auth/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ AUTHENTICATION__JWT_ALGORITHM=RS256
AUTHENTICATION__ACCESS_TOKEN_VALIDITY_MINUTES=5
AUTHENTICATION__REFRESH_TOKEN_VALIDITY_DAYS=7
AUTHENTICATION__ACTIVE_USERNAMES_PATH=./active_usernames.txt
MAINTENANCE__MAINTENANCE_PATH=./maintenance/maintenance.json
MAINTENANCE__SCHEDULED_MAINTENANCE_PATH=./maintenance/scheduled_maintenance.json
LDAP_SERVER__URL=ldaps://ldap.example.com:636
LDAP_SERVER__REALM=LDAP.EXAMPLE.COM
LDAP_SERVER__CERTIFICATE_VALIDATION=true
Expand Down
7 changes: 7 additions & 0 deletions ldap_jwt_auth/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ class APIConfig(BaseModel):
allowed_cors_origins: List[str]
allowed_cors_methods: List[str]

class MaintenanceConfig(BaseModel):
"""
Configuration model for maintenance
"""
maintenance_path: str
scheduled_maintenance_path: str

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got picked up by the black formatter.

Suggested change
class MaintenanceConfig(BaseModel):
"""
Configuration model for maintenance
"""
maintenance_path: str
scheduled_maintenance_path: str
class MaintenanceConfig(BaseModel):
"""
Configuration model for maintenance
"""
maintenance_path: str
scheduled_maintenance_path: str

class AuthenticationConfig(BaseModel):
"""
Expand Down Expand Up @@ -82,6 +88,7 @@ class Config(BaseSettings):
api: APIConfig
authentication: AuthenticationConfig
ldap_server: LDAPServerConfig
maintenance: MaintenanceConfig

model_config = SettingsConfigDict(
env_file=Path(__file__).parent.parent / ".env",
Expand Down
12 changes: 12 additions & 0 deletions ldap_jwt_auth/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,15 @@ class UsernameMismatchError(Exception):
"""
Exception raised when the usernames in the access and refresh tokens do not match.
"""


class InvalidMaintenanceFileError(Exception):
"""
Exception raised when the maintenance state files do not have the correct format or value types.
"""

MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved

class MissingMaintenanceFileError(Exception):
"""
Exception raised when the maintenance state file is missing or its data cannot be read.
"""
49 changes: 49 additions & 0 deletions ldap_jwt_auth/core/maintenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
Module for handling maintenance mode
"""

import json

from pydantic import ValidationError
from ldap_jwt_auth.core.config import config
from ldap_jwt_auth.core.exceptions import InvalidMaintenanceFileError, MissingMaintenanceFileError
from ldap_jwt_auth.core.schemas import MaintenanceState, ScheduledMaintenanceState

class Maintenance:
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved
"""
Class for managing maintenance and scheduled maintenance states.
"""

def get_maintenance_state(self) -> MaintenanceState:
"""
Return the maintenance state of the system

:return: Maintenance state schema
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved
:raises InvalidFileFormat: If the maintenance state file is incorrectly formatted
:raises MissingMaintenanceFileError: If the maintenance state file can not be found or read
"""
try:
with open(config.maintenance.maintenance_path, "r", encoding='utf-8') as file:
data = json.load(file)
return MaintenanceState(**data)
except (OSError, json.JSONDecodeError, TypeError) as exc:
raise MissingMaintenanceFileError("Unable to find maintenance file") from exc
except ValidationError as exc:
raise InvalidMaintenanceFileError("Maintenance file format is incorrect") from exc
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved

def get_scheduled_maintenance_state(self) -> ScheduledMaintenanceState:
"""
Return the scheduled maintenance state of the system

:return: Scheduled maintenance state schema
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved
:raises InvalidFileFormat: If the scheduled maintenance state file is incorrectly formatted
:raises MissingMaintenanceFileError: If the scheduled maintenance state file can not be found or read
"""
try:
with open(config.maintenance.scheduled_maintenance_path, "r", encoding='utf-8') as file:
data = json.load(file)
return ScheduledMaintenanceState(**data)
except (OSError, json.JSONDecodeError, TypeError) as exc:
raise MissingMaintenanceFileError("Unable to find scheduled maintenance file") from exc
except ValidationError as exc:
raise InvalidMaintenanceFileError("Scheduled maintenance file format is incorrect") from exc
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved
15 changes: 15 additions & 0 deletions ldap_jwt_auth/core/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,18 @@ class UserCredentialsPostRequestSchema(BaseModel):
password: SecretStr

model_config = ConfigDict(hide_input_in_errors=True)


class MaintenanceState(BaseModel):
"""
Model for maintenance response
"""

show: bool
message: str


class ScheduledMaintenanceState(MaintenanceState):
"""
Model for scheduled maintenance state
"""
3 changes: 2 additions & 1 deletion ldap_jwt_auth/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from ldap_jwt_auth.core.config import config
from ldap_jwt_auth.core.logger_setup import setup_logger
from ldap_jwt_auth.routers import login, refresh, verify
from ldap_jwt_auth.routers import login, maintenance, refresh, verify

app = FastAPI(title=config.api.title, description=config.api.description, root_path=config.api.root_path)

Expand Down Expand Up @@ -66,6 +66,7 @@ async def custom_validation_exception_handler(_: Request, exc: RequestValidation
app.include_router(login.router)
app.include_router(refresh.router)
app.include_router(verify.router)
app.include_router(maintenance.router)


@app.get("/")
Expand Down
54 changes: 54 additions & 0 deletions ldap_jwt_auth/routers/maintenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Module for providing an API router which defines maintenance route(s)
"""

import logging
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, status
from ldap_jwt_auth.core.exceptions import InvalidMaintenanceFileError, MissingMaintenanceFileError
from ldap_jwt_auth.core.maintenance import Maintenance
from ldap_jwt_auth.core.schemas import MaintenanceState, ScheduledMaintenanceState

logger = logging.getLogger()

router = APIRouter(tags=["maintenance"])


@router.get(
path="/maintenance", summary="Get the maintenance state", response_description="Returns the maintenance state"
)
def get_maintenance_state(maintenance: Annotated[Maintenance, Depends(Maintenance)]) -> MaintenanceState:
# pylint: disable=missing-function-docstring
logger.info("Getting maintenance state")

try:
return maintenance.get_maintenance_state()

except MissingMaintenanceFileError as exc:
message = "Unable to find maintenance file"
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=message) from exc
except InvalidMaintenanceFileError as exc:
message = "Maintenance file format is incorrect"
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=message) from exc
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved


@router.get(
path="/scheduled_maintenance",
summary="Get the scheduled maintenance state",
response_description="Returns the scheduled maintenance state",
)
def get_scheduled_maintenance_state(
maintenance: Annotated[Maintenance, Depends(Maintenance)]
) -> ScheduledMaintenanceState:
# pylint: disable=missing-function-docstring
logger.info("Getting scheduled maintenance state")
try:
return maintenance.get_scheduled_maintenance_state()

except MissingMaintenanceFileError as exc:
message = "Unable to find scheduled maintenance file"
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=message) from exc
except InvalidMaintenanceFileError as exc:
message = "Scheduled maintenance file format is incorrect"
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=message) from exc
MatteoGuarnaccia5 marked this conversation as resolved.
Show resolved Hide resolved
Loading