From be61199bd1ef43776670c0aebbe48a013f97a154 Mon Sep 17 00:00:00 2001 From: VKTB <45173816+VKTB@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:19:46 +0000 Subject: [PATCH 1/7] Update logging example file to store logs in logs dir #179 --- inventory_management_system_api/logging.example.ini | 2 +- logs/.keep | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 logs/.keep diff --git a/inventory_management_system_api/logging.example.ini b/inventory_management_system_api/logging.example.ini index 377a52e0..2f50b2c8 100644 --- a/inventory_management_system_api/logging.example.ini +++ b/inventory_management_system_api/logging.example.ini @@ -22,7 +22,7 @@ propagate=0 [handler_fileHandler] class=logging.handlers.TimedRotatingFileHandler formatter=fileFormatter -args=('inventory-management-system-api.log', 'D', 1, 20,) +args=('./logs/inventory-management-system-api.log', 'D', 1, 20,) [handler_consoleHandler] class=StreamHandler diff --git a/logs/.keep b/logs/.keep new file mode 100644 index 00000000..e69de29b From a49dffcf165281f74c7a7a4bef6f5b6a5c30f4dd Mon Sep 17 00:00:00 2001 From: VKTB <45173816+VKTB@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:22:55 +0000 Subject: [PATCH 2/7] Copy logs dir in Docker image and make it writable #179 --- Dockerfile.prod | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Dockerfile.prod b/Dockerfile.prod index f73daa30..2bf8a3a8 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -5,6 +5,7 @@ WORKDIR /inventory-management-system-api-run COPY README.md pyproject.toml ./ # Copy inventory_management_system_api source files COPY inventory_management_system_api/ inventory_management_system_api/ +COPY logs/ logs/ RUN set -eux; \ \ @@ -18,10 +19,8 @@ RUN set -eux; \ addgroup -S inventory-management-system-api; \ adduser -S -D -G inventory-management-system-api -H -h /inventory-management-system-api-run inventory-management-system-api; \ \ - # Create a log file \ - touch inventory-management-system-api.log; \ - # Change ownership of log file - app will need to write to it - chown -R inventory-management-system-api:inventory-management-system-api inventory-management-system-api.log; + # Change ownership of logs/ - app will need to write log files to it \ + chown -R inventory-management-system-api:inventory-management-system-api logs/; USER inventory-management-system-api From 9e18309f20428efbd5da606d80ce88adf0c114b1 Mon Sep 17 00:00:00 2001 From: VKTB <45173816+VKTB@users.noreply.github.com> Date: Fri, 9 Feb 2024 12:21:41 +0000 Subject: [PATCH 3/7] Remove trailing slash from endpoint paths #179 --- .../routers/v1/catalogue_category.py | 4 ++-- inventory_management_system_api/routers/v1/catalogue_item.py | 4 ++-- inventory_management_system_api/routers/v1/item.py | 4 ++-- inventory_management_system_api/routers/v1/manufacturer.py | 4 ++-- inventory_management_system_api/routers/v1/system.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/inventory_management_system_api/routers/v1/catalogue_category.py b/inventory_management_system_api/routers/v1/catalogue_category.py index ed48579b..8744be67 100644 --- a/inventory_management_system_api/routers/v1/catalogue_category.py +++ b/inventory_management_system_api/routers/v1/catalogue_category.py @@ -31,7 +31,7 @@ router = APIRouter(prefix="/v1/catalogue-categories", tags=["catalogue categories"]) -@router.get(path="/", summary="Get catalogue categories", response_description="List of catalogue categories") +@router.get(path="", summary="Get catalogue categories", response_description="List of catalogue categories") def get_catalogue_categories( parent_id: Annotated[Optional[str], Query(description="Filter catalogue categories by parent ID")] = None, catalogue_category_service: CatalogueCategoryService = Depends(), @@ -97,7 +97,7 @@ def get_catalogue_category_breadcrumbs( @router.post( - path="/", + path="", summary="Create a new catalogue category", response_description="The created catalogue category", status_code=status.HTTP_201_CREATED, diff --git a/inventory_management_system_api/routers/v1/catalogue_item.py b/inventory_management_system_api/routers/v1/catalogue_item.py index dd7889c6..b2edc7e1 100644 --- a/inventory_management_system_api/routers/v1/catalogue_item.py +++ b/inventory_management_system_api/routers/v1/catalogue_item.py @@ -28,7 +28,7 @@ router = APIRouter(prefix="/v1/catalogue-items", tags=["catalogue items"]) -@router.get(path="/", summary="Get catalogue items", response_description="List of catalogue items") +@router.get(path="", summary="Get catalogue items", response_description="List of catalogue items") def get_catalogue_items( catalogue_category_id: Annotated[ Optional[str], Query(description="Filter catalogue items by catalogue category ID") @@ -69,7 +69,7 @@ def get_catalogue_item( @router.post( - path="/", + path="", summary="Create a new catalogue item", response_description="The created catalogue item", status_code=status.HTTP_201_CREATED, diff --git a/inventory_management_system_api/routers/v1/item.py b/inventory_management_system_api/routers/v1/item.py index 1c687b02..51f6b485 100644 --- a/inventory_management_system_api/routers/v1/item.py +++ b/inventory_management_system_api/routers/v1/item.py @@ -24,7 +24,7 @@ @router.post( - path="/", + path="", summary="Create a new item", response_description="The created item", status_code=status.HTTP_201_CREATED, @@ -77,7 +77,7 @@ def delete_item( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=message) from exc -@router.get(path="/", summary="Get items", response_description="List of items") +@router.get(path="", summary="Get items", response_description="List of items") def get_items( item_service: Annotated[ItemService, Depends(ItemService)], system_id: Annotated[Optional[str], Query(description="Filter items by system ID")] = None, diff --git a/inventory_management_system_api/routers/v1/manufacturer.py b/inventory_management_system_api/routers/v1/manufacturer.py index a9b7c14b..11c13d78 100644 --- a/inventory_management_system_api/routers/v1/manufacturer.py +++ b/inventory_management_system_api/routers/v1/manufacturer.py @@ -27,7 +27,7 @@ @router.post( - path="/", + path="", summary="Create new manufacturer", response_description="The new manufacturer", status_code=status.HTTP_201_CREATED, @@ -51,7 +51,7 @@ def create_manufacturer( @router.get( - path="/", + path="", summary="Get all manufacturers", response_description="List of manufacturers", ) diff --git a/inventory_management_system_api/routers/v1/system.py b/inventory_management_system_api/routers/v1/system.py index b164503b..7091963e 100644 --- a/inventory_management_system_api/routers/v1/system.py +++ b/inventory_management_system_api/routers/v1/system.py @@ -26,7 +26,7 @@ @router.post( - path="/", + path="", summary="Create a new System", response_description="The created System", status_code=status.HTTP_201_CREATED, @@ -50,7 +50,7 @@ def create_system( raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=message) from exc -@router.get(path="/", summary="Get Systems", response_description="List of Systems") +@router.get(path="", summary="Get Systems", response_description="List of Systems") def get_systems( system_service: Annotated[SystemService, Depends(SystemService)], parent_id: Annotated[Optional[str], Query(description="Filter Systems by parent ID")] = None, From d19d05d7cb719183277845e12c03e35007b2397a Mon Sep 17 00:00:00 2001 From: VKTB <45173816+VKTB@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:15:27 +0000 Subject: [PATCH 4/7] Update README #179 --- README.md | 189 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 138 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index d7e6ea0b..6cc477cd 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,176 @@ # Inventory Management System ## How to Run - This is a Python microservice created using FastAPI and requires a MongoDB instance to run against. ### Prerequisites -- Docker installed (if you want to run the microservice inside Docker) -- Python 3.10 (or above) and MongoDB 6.0 installed on your machine if you are not using Docker -- [MongoDB Compass](https://www.mongodb.com/products/compass) installed (if you want to interact with the database using a GUI) +- Docker and Docker Compose installed (if you want to run the microservice inside Docker) +- Python 3.10 (or above) and MongoDB 6.0 installed on your machine (if you are not using Docker) +- Public key (must be OpenSSH encoded) to decode JWT access tokens (if JWT authentication/authorization is enabled) +- [MongoDB Compass](https://www.mongodb.com/products/compass) installed (if you want to interact with the database using + a GUI) - This repository cloned ### Docker Setup -The easiest way to run the application with Docker for local development is using the `docker-compose.yml` file. It is -configured to spin up a MongoDB instance that can be accessed at `localhost:27017` using `root` as the username and -`example` as the password. It also starts the application in a reload mode using the `Dockerfile`. Use the `Dockerfile` -or `Dockerfile.prod` to run just the application itself in a container. The former is for local development and must -not be used in production. - 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 .env.example .env - ``` +1. Create a `.env` file alongside the `.env.example` file. Use the example file as a reference and modify the values + accordingly. + ```bash + cp inventory_management_system_api/.env.example inventory_management_system_api/.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 logging.example.ini logging.ini - ``` +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 inventory_management_system_api/logging.example.ini inventory_management_system_api/logging.ini + ``` -3. (**Required only if JWT Auth is enabled**) Create a `keys` directory in the root of the project directory and inside it create a copy of the public key generated by the authentication component. This is needed for decoding of JWT access tokens signed by the corresponding private key. +3. (**Required only if JWT Auth is enabled**) Create a `keys` directory in the root of the project directory and inside + it create a copy of the public key generated by the authentication component. This is needed for decoding of JWT + access tokens signed by the corresponding private key. -#### Using Docker Compose File +#### 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 spin up a MongoDB instance that can be accessed at `localhost:27017` using `root` as the username and +`example` as the password. It also starts the application in a reload mode using the `Dockerfile`. 1. Build and start the Docker containers: - ```bash - docker-compose up - ``` + ```bash + docker-compose up + ``` The microservice should now be running inside Docker at http://localhost:8000 and its Swagger UI could be accessed at http://localhost:8000/docs. A MongoDB instance should also be running at http://localhost:27017. -#### Using Dockerfiles +#### 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 either the `Dockerfile` or `Dockerfile.prod` from the root of the project directory: - ```bash - docker build -f Dockerfile.prod -t ims_api_image . - ``` +1. Build an image using the `Dockerfile` from the root of the project directory: + ```bash + docker build -f Dockerfile -t inventory_management_system_api_image . + ``` 2. Start the container using the image built and map it to port `8000` locally: - ```bash - docker run -p 8000:8000 --name ims_api_container ims_api_image - ``` + ```bash + docker run -p 8000:8000 --name inventory_management_system_api_container -v ./keys/jwt-key.pub:/inventory-management-system-api-run/keys/jwt-key.pub -v ./logs:/inventory-management-system-api-run/logs inventory_management_system_api_image + ``` or with values for the environment variables: - ```bash - docker run -p 8000:8000 --name ims_api_container --env DATABASE__NAME=test-ims ims_api_image - ``` + ```bash + docker run -p 8000:8000 --name inventory_management_system_api_container --env DATABASE__NAME=test-ims -v ./keys/jwt-key.pub:/inventory-management-system-api-run/keys/jwt-key.pub -v ./logs:/inventory-management-system-api-run/logs inventory_management_system_api_image + ``` The microservice should now be running inside Docker at http://localhost:8000 and its Swagger UI could be accessed at http://localhost:8000/docs. -### Local Setup +#### Using `Dockerfile.prod` +Use the `Dockerfile.prod` to run just the application itself in a container. This can be used for production. + +1. While in root of the project directory, change the permissions of the `logs` directory so that it is writable by + other users. This allows the container to save the application logs to it. + ```bash + sudo chmod -R 0777 logs/ + ``` -Ensure that MongoDB is installed and running on your machine before proceeding. +2. Build an image using the `Dockerfile.prod` from the root of the project directory: + ```bash + docker build -f Dockerfile.prod -t inventory_management_system_api_image . + ``` + +3. Start the container using the image built and map it to port `8000` locally: + ```bash + docker run -p 8000:8000 --name inventory_management_system_api_container -v ./keys/jwt-key.pub:/inventory-management-system-api-run/keys/jwt-key.pub -v ./logs:/inventory-management-system-api-run/logs inventory_management_system_api_image + ``` + or with values for the environment variables: + ```bash + docker run -p 8000:8000 --name inventory_management_system_api_container --env DATABASE__NAME=test-ims -v ./keys/jwt-key.pub:/inventory-management-system-api-run/keys/jwt-key.pub -v ./logs:/inventory-management-system-api-run/logs inventory_management_system_api_image + ``` + The microservice should now be running inside Docker at http://localhost:8000 and its Swagger UI could be accessed + 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 - ``` + ```bash + python -m venv venv + source venv/bin/activate + ``` + 2. Install the required dependencies using pip: - ```bash - pip install .[dev] - ``` -3. Create a `.env` file using the `.env.example` file and modify the values inside accordingly. -4. Create a `logging.ini` file using the `logging.example.ini` file and modify it accordingly. -5. Start the microservice using Uvicorn: - ```bash - uvicorn inventory_management_system_api.main:app --log-config inventory_management_system_api/logging.ini --reload - ``` + ```bash + pip install .[dev] + ``` + +3. Create a `.env` file alongside the `.env.example` file. Use the example file as a reference and modify the values + accordingly. + ```bash + cp inventory_management_system_api/.env.example inventory_management_system_api/.env + ``` + +4. Create a `logging.ini` file alongside the `logging.example.ini` file. Use the example file as a reference and modify + it accordingly: + ```bash + cp inventory_management_system_api/logging.example.ini inventory_management_system_api/logging.ini + ``` + +5. (**Required only if JWT Auth is enabled**) Create a `keys` directory in the root of the project directory and inside + it create a copy of the public key generated by the authentication component. This is needed for decoding of JWT + access tokens signed by the corresponding private key. + +6. Start the microservice using Uvicorn: + ```bash + uvicorn inventory_management_system_api.main:app --log-config inventory_management_system_api/logging.ini --reload + ``` The microservice should now be running locally at http://localhost:8000. The Swagger UI can be accessed at http://localhost:8000/docs. -6. To run the unit tests, run : + +7. To run the unit tests, run : ```bash pytest -c test/pytest.ini test/unit/ ``` -7. To run the e2e tests, run: + +8. To run the e2e tests, run: ```bash pytest -c test/pytest.ini test/e2e/ ``` + +9. To run all the tests, run: + ```bash + pytest -c test/pytest.ini test/ + ``` + +## Notes + +### JWT Authentication/Authorization +This microservice supports JWT authentication/authorization and this can be enabled or disabled by setting +the `AUTHENTICATION__ENABLED` environment variable to `True` or `False`. When enabled, all the endpoints require a JWT +access token to be supplied. This ensures that only authenticated and authorized users can access the resources. To +decode the JWT access token, the application needs the public key that corresponding to the private key used for +encoding the token. Once the JWT access token is decoded successfully, it checks that it has a `username` in the +payload, and it has not expired. This means that any microservice can be used to generate JWT access tokens so long as +it meets the above criteria. The [LDAP-JWT Authentication Service](https://github.com/ral-facilities/ldap-jwt-auth) is +a microservice that provides user authentication against an LDAP server and returns a JWT access token. + +### 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 +still read environment variables as well as the `.env` file, environment variables will always take priority over +values loaded from the `.env` file. + +Listed below are the environment variables supported by the application. + +| Environment Variable | Description | Mandatory | Default Value | +|-----------------------------------|--------------------------------------------------------------------------------------------------------------|---------------------|-------------------------------------------------------| +| `API__TITLE` | The title of the API which is added to the generated OpenAPI. | No | `Inventory Management System API` | +| `API__DESCRIPTION` | The description of the API which is added to the generated OpenAPI. | No | `This is the API for the Inventory Management System` | +| `API__ROOT_PATH` | (If using a proxy) The path prefix handled by a proxy that is not seen by the app. | No | ` ` | +| `AUTHENTICATION__ENABLED` | Whether JWT auth is enabled. | Yes | `False` | +| `AUTHENTICATION__PUBLIC_KEY_PATH` | The path to the public key to be used for decoding JWT access token signed by the corresponding private key. | If JWT auth enabled | | +| `AUTHENTICATION__JWT_ALGORITHM` | The algorithm to use to decode the JWT access token. | If JWT auth enabled | | +| `DATABASE__PROTOCOL` | The protocol of the database to use for the `MongoClient` to connect to the database. | Yes | | +| `DATABASE__USERNAME` | The username of the database user for the `MongoClient` to connect to the database. | Yes | | +| `DATABASE__PASSWORD` | The password of the database user for the `MongoClient` to connect to the database. | Yes | | +| `DATABASE__HOSTNAME` | The hostname of the database to use for the `MongoClient` to connect to the database. | Yes | | +| `DATABASE__PORT` | The port of the database to use for the `MongoClient` to connect to the database. | Yes | | +| `DATABASE__NAME` | The name of the database to use for the `MongoClient` to connect to the database. | Yes | | From 3726306e388eee6045c580e647b5f16f4cf59138 Mon Sep 17 00:00:00 2001 From: VKTB <45173816+VKTB@users.noreply.github.com> Date: Tue, 13 Feb 2024 09:55:29 +0000 Subject: [PATCH 5/7] Add keys directory #179 --- .gitignore | 2 +- keys/.keep | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 keys/.keep diff --git a/.gitignore b/.gitignore index 5ae89f08..b5167530 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ mongodb/data/* !mongodb/data/.keep inventory_management_system_api/logging.ini -/keys/ +/keys/jwt-key.pub # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/keys/.keep b/keys/.keep new file mode 100644 index 00000000..e69de29b From 2fa6e4398c3d8506a380354c616d1101ae2c08bf Mon Sep 17 00:00:00 2001 From: VKTB <45173816+VKTB@users.noreply.github.com> Date: Tue, 13 Feb 2024 09:56:08 +0000 Subject: [PATCH 6/7] Remove env var default values from Dockerfiles #179 --- Dockerfile | 10 ---------- Dockerfile.prod | 10 ---------- 2 files changed, 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index a66dae4f..6448f7a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,15 +10,5 @@ RUN --mount=type=cache,target=/root/.cache \ \ python3 -m pip install .; -ENV API__TITLE="Inventory Management System API" -ENV API__DESCRIPTION="This is the API for the Inventory Management System" -ENV API__ROOT_PATH="" -ENV DATABASE__PROTOCOL=mongodb -ENV DATABASE__USERNAME=root -ENV DATABASE__PASSWORD=example -ENV DATABASE__HOSTNAME=localhost -ENV DATABASE__PORT=27017 -ENV DATABASE__NAME=ims - CMD ["uvicorn", "inventory_management_system_api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] EXPOSE 8000 diff --git a/Dockerfile.prod b/Dockerfile.prod index 2bf8a3a8..0c4dfbdf 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -24,15 +24,5 @@ RUN set -eux; \ USER inventory-management-system-api -ENV API__TITLE="Inventory Management System API" -ENV API__DESCRIPTION="This is the API for the Inventory Management System" -ENV API__ROOT_PATH="" -ENV DATABASE__PROTOCOL=mongodb -ENV DATABASE__USERNAME=root -ENV DATABASE__PASSWORD=example -ENV DATABASE__HOSTNAME=localhost -ENV DATABASE__PORT=27017 -ENV DATABASE__NAME=ims - CMD ["uvicorn", "inventory_management_system_api.main:app", "--host", "0.0.0.0", "--port", "8000"] EXPOSE 8000 From df9851ad61fe7199484516eee7ad427d07a93243 Mon Sep 17 00:00:00 2001 From: VKTB <45173816+VKTB@users.noreply.github.com> Date: Tue, 13 Feb 2024 09:58:27 +0000 Subject: [PATCH 7/7] Update README #179 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7afd04e7..4323a25e 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ production)! docker build -f Dockerfile -t inventory_management_system_api_image . ``` -2. Start the container using the image built and map it to port `8000` locally: +2. Start the container using the image built and map it to port `8000` locally (please note that the public key volume + is only needed if JWT Auth is enabled): ```bash docker run -p 8000:8000 --name inventory_management_system_api_container -v ./keys/jwt-key.pub:/inventory-management-system-api-run/keys/jwt-key.pub -v ./logs:/inventory-management-system-api-run/logs inventory_management_system_api_image ```