diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 0a886c47..ff383db5 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -31,6 +31,15 @@ jobs: run: | sudo apt update sudo apt install python3-dev python3-pip + - name: Install MongoDB + run: | + sudo apt-get install -y gnupg wget + sudo wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add - + sudo echo "deb http://repo.mongodb.org/apt/debian buster/mongodb-org/4.4 main" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list + sudo apt-get update + sudo apt-get install -y mongodb-org + - name: Start MongoDB + run: sudo systemctl start mongod - name: Install dependencies run: | python -m pip install --upgrade pip @@ -39,7 +48,7 @@ jobs: if [ -f requirements-customizations.txt ]; then pip install -r requirements-customizations.txt; fi python -m pip install -U setuptools python -m pip install -e . - python -m pip install "Pillow>=10.0.0,<10.1" "device_detector>=5.0,<6" "satosa>=8.4,<8.6" "jinja2>=3.0,<4" + python -m pip install "Pillow>=10.0.0,<10.1" "device_detector>=5.0,<6" "satosa>=8.4,<8.6" "jinja2>=3.0,<4" "pymongo>=4.4.1,<4.5" - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/example/docker-compose.yml b/example/docker-compose.yml index a2b1aae8..dace46b3 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -18,19 +18,20 @@ services: networks: - wordpress-network - phpmyadmin: - depends_on: - - database - image: arm64v8/phpmyadmin:5.2.1-apache - restart: unless-stopped - ports: - - 8081:80 - env_file: .env - environment: - PMA_HOST: database - MYSQL_ROOT_PASSWORD: '${MYSQL_ROOT_PASSWORD}' - networks: - - wordpress-network + #Decomment this sectiom if you need phpmyadmin + #phpmyadmin: + # depends_on: + # - database + # image: phpmyadmin:5.2.1-apache + # restart: unless-stopped + # ports: + # - 8081:80 + # env_file: .env + # environment: + # PMA_HOST: database + # MYSQL_ROOT_PASSWORD: '${MYSQL_ROOT_PASSWORD}' + # networks: + # - wordpress-network wordpress: depends_on: diff --git a/example/satosa/pyeudiw_backend.yaml b/example/satosa/pyeudiw_backend.yaml index b83ff33d..78fbeae7 100644 --- a/example/satosa/pyeudiw_backend.yaml +++ b/example/satosa/pyeudiw_backend.yaml @@ -59,6 +59,18 @@ config: x: TSO-KOqdnUj5SUuasdlRB2VVFSqtJOxuR5GftUTuBdk y: ByWgQt1wGBSnF56jQqLdoO1xKUynMY-BHIDB3eXlR7 + # Mongodb database configuration + mongo_db_settings: + cache: + url: mongodb://localhost:27017/ + conf: + db_name: eudiw + storage: + url: mongodb://localhost:27017/ + conf: + db_name: eudiw + db_collection: sessions + #This is the configuration for the relaying party metadata metadata: application_type: web diff --git a/pyeudiw/storage/__init__.py b/pyeudiw/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyeudiw/storage/base_cache.py b/pyeudiw/storage/base_cache.py new file mode 100644 index 00000000..67455ea0 --- /dev/null +++ b/pyeudiw/storage/base_cache.py @@ -0,0 +1,8 @@ +from typing import Callable + +class BaseCache(): + def try_retrieve(self, object_name: str, on_not_found: Callable[[], str]) -> dict: + raise NotImplementedError() + + def overwrite(self, object_name: str, value_gen_fn: Callable[[], str]) -> dict: + raise NotImplementedError() \ No newline at end of file diff --git a/pyeudiw/storage/base_storage.py b/pyeudiw/storage/base_storage.py new file mode 100644 index 00000000..452d17cc --- /dev/null +++ b/pyeudiw/storage/base_storage.py @@ -0,0 +1,9 @@ +class BaseStorage(object): + def init_session(self, dpop_proof: dict, attestation: dict): + NotImplementedError() + + def update_request_object(self, document_id: str, request_object: dict): + NotImplementedError() + + def update_response_object(self, nonce: str, state: str, response_object: dict): + NotImplementedError() \ No newline at end of file diff --git a/pyeudiw/storage/mongo_cache.py b/pyeudiw/storage/mongo_cache.py new file mode 100644 index 00000000..fffaa122 --- /dev/null +++ b/pyeudiw/storage/mongo_cache.py @@ -0,0 +1,64 @@ +import pymongo +from datetime import datetime +from typing import Callable + +from .base_cache import BaseCache + +class MongoCache(BaseCache): + def __init__(self, storage_conf: dict, url: str, connection_params: dict = None) -> None: + super().__init__() + + self.storage_conf = storage_conf + self.url = url + self.connection_params = connection_params + + self.client = None + self.db = None + + def _connect(self): + if not self.client or not self.client.server_info(): + self.client = pymongo.MongoClient(self.url, **self.connection_params) + self.db = getattr(self.client, self.storage_conf["db_name"]) + self.collection = getattr(self.db, "cache_storage") + + def try_retrieve(self, object_name: str, on_not_found: Callable[[], str]) -> dict: + self._connect() + + query = {"object_name": object_name} + + cache_object = self.collection.find_one(query) + + if cache_object is None: + creation_date = datetime.timestamp(datetime.now()) + cache_object = { + "object_name": object_name, + "data": on_not_found(), + "creation_date": creation_date + } + + self.collection.insert_one(cache_object) + + return cache_object + + def overwrite(self, object_name: str, value_gen_fn: Callable[[], str]) -> dict: + self._connect() + + new_data = value_gen_fn() + updated_date = datetime.timestamp(datetime.now()) + + cache_object = { + "object_name": object_name, + "data": new_data, + "creation_date": updated_date + } + + query = {"object_name": object_name} + + self.collection.update_one(query, { + "$set": { + "data": new_data, + "creation_date": updated_date + } + }) + + return cache_object \ No newline at end of file diff --git a/pyeudiw/storage/mongo_storage.py b/pyeudiw/storage/mongo_storage.py new file mode 100644 index 00000000..a3e22779 --- /dev/null +++ b/pyeudiw/storage/mongo_storage.py @@ -0,0 +1,92 @@ +import pymongo +from datetime import datetime + +from .base_storage import BaseStorage + +class MongoStorage(BaseStorage): + def __init__(self, storage_conf: dict, url: str, connection_params: dict = None) -> None: + super().__init__() + + self.storage_conf = storage_conf + self.url = url + self.connection_params = connection_params + + self.client = None + self.db = None + + def _connect(self): + if not self.client or not self.client.server_info(): + self.client = pymongo.MongoClient(self.url, **self.connection_params) + self.db = getattr(self.client, self.storage_conf["db_name"]) + self.collection = getattr(self.db, self.storage_conf["db_collection"]) + + def _retrieve_document_by_id(self, document_id: str) -> dict: + self._connect() + + document = self.collection.find_one({"_id": document_id}) + + if document is None: + raise ValueError(f'Document with id {document_id} not found') + + return document + + def _retrieve_document_by_nonce_state(self, nonce: str, state: str) -> dict: + self._connect() + + query = {"state": state, "nonce": nonce} + + document = self.collection.find_one(query) + + if document is None: + raise ValueError(f'Document with nonce {nonce} and state {state} not found') + + return document + + def init_session(self, dpop_proof: dict, attestation: dict): + creation_date = datetime.timestamp(datetime.now()) + + entity = { + "creation_date": creation_date, + "dpop_proof": dpop_proof, + "attestation": attestation, + "request_object": None, + "response": None + } + + self._connect() + document_id = self.collection.insert_one(entity) + + return document_id.inserted_id + + def update_request_object(self, document_id: str, request_object: dict): + nonce = request_object["nonce"] + state = request_object["state"] + + self._connect() + documentStatus = self.collection.update_one( + {"_id": document_id}, + { + "$set": { + "nonce": nonce, + "state": state, + "request_object": request_object + } + } + ) + + return nonce, state, documentStatus + + def update_response_object(self, nonce: str, state: str, response_object: dict): + document = self._retrieve_document_by_nonce_state(nonce, state) + + document_id = document["_id"] + + documentStatus = self.collection.update_one( + {"_id": document_id}, + {"$set": + { + "response_object": response_object + }, + }) + + return nonce, state, documentStatus \ No newline at end of file diff --git a/pyeudiw/tests/storage/__init__.py b/pyeudiw/tests/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyeudiw/tests/storage/test_mongo_cache.py b/pyeudiw/tests/storage/test_mongo_cache.py new file mode 100644 index 00000000..f9146822 --- /dev/null +++ b/pyeudiw/tests/storage/test_mongo_cache.py @@ -0,0 +1,49 @@ +import uuid +import pytest + +from pyeudiw.storage.mongo_cache import MongoCache + +class TestMongoCache: + @pytest.fixture(autouse=True) + def create_storage_instance(self): + self.cache = MongoCache( + {"db_name": "eudiw"}, + "mongodb://localhost:27017/", + {} + ) + + def test_try_retrieve(self): + object_name = str(uuid.uuid4()) + data = str(uuid.uuid4()) + + obj = self.cache.try_retrieve(object_name, lambda : data) + + assert obj + assert obj["object_name"] == object_name + assert obj["data"] == data + assert obj["creation_date"] + + query = {"object_name": object_name} + + cache_object = self.cache.collection.find_one(query) + + assert obj == cache_object + + def test_overwrite(self): + object_name = str(uuid.uuid4()) + data = str(uuid.uuid4()) + + obj = self.cache.try_retrieve(object_name, lambda : data) + + data_updated = str(uuid.uuid4()) + + updated_obj = self.cache.overwrite(object_name, lambda : data_updated) + + assert obj["data"] != updated_obj["data"] + assert obj["creation_date"] != updated_obj["creation_date"] + + query = {"object_name": object_name} + cache_object = self.cache.collection.find_one(query) + + assert cache_object["data"] == updated_obj["data"] + assert cache_object["creation_date"] == updated_obj["creation_date"] \ No newline at end of file diff --git a/pyeudiw/tests/storage/test_mongo_storage.py b/pyeudiw/tests/storage/test_mongo_storage.py new file mode 100644 index 00000000..79785307 --- /dev/null +++ b/pyeudiw/tests/storage/test_mongo_storage.py @@ -0,0 +1,89 @@ +import uuid +import pytest + +from pyeudiw.storage.mongo_storage import MongoStorage + +class TestMongoStorage: + @pytest.fixture(autouse=True) + def create_storage_instance(self): + self.storage = MongoStorage( + {"db_name": "eudiw", "db_collection": "test"}, + "mongodb://localhost:27017/", + {} + ) + + def test_mongo_connection(self): + self.storage._connect() + + assert self.storage.db is not None + assert self.storage.client + assert self.storage.collection is not None + + def test_entity_initialization(self): + document_id = self.storage.init_session({"dpop": "test"}, {"attestation": "test"}) + + assert document_id + + document = self.storage._retrieve_document_by_id(document_id) + + assert document + assert document["dpop_proof"] + assert document["dpop_proof"] == {"dpop": "test"} + assert document["attestation"] + assert document["attestation"] == {"attestation": "test"} + + def test_add_request_object(self): + document_id = self.storage.init_session({"dpop": "test"}, {"attestation": "test"}) + + assert document_id + + nonce = str(uuid.uuid4()) + state = str(uuid.uuid4()) + + request_object = {"nonce": nonce, "state": state} + + self.storage.update_request_object(document_id, request_object) + + document = self.storage._retrieve_document_by_id(document_id) + + assert document + assert document["dpop_proof"] + assert document["dpop_proof"] == {"dpop": "test"} + assert document["attestation"] + assert document["attestation"] == {"attestation": "test"} + assert document["state"] + assert document["state"] == state + assert document["state"] + assert document["nonce"] == nonce + assert document["request_object"] == request_object + + def test_update_responnse_object(self): + document_id = self.storage.init_session({"dpop": "test"}, {"attestation": "test"}) + + assert document_id + + nonce = str(uuid.uuid4()) + state = str(uuid.uuid4()) + + request_object = {"nonce": nonce, "state": state} + + self.storage.update_request_object(document_id, request_object) + documentStatus = self.storage.update_response_object(nonce, state, {"response": "test"}) + + assert documentStatus + + document = self.storage._retrieve_document_by_id(document_id) + + assert document + assert document["dpop_proof"] + assert document["dpop_proof"] == {"dpop": "test"} + assert document["attestation"] + assert document["attestation"] == {"attestation": "test"} + assert document["state"] + assert document["state"] == state + assert document["state"] + assert document["nonce"] == nonce + assert document["request_object"] == request_object + assert document["response_object"] + assert document["response_object"] == {"response": "test"} + \ No newline at end of file diff --git a/setup.py b/setup.py index a4b4c0ef..59f42eb9 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,8 @@ def readme(): "Pillow>=10.0.0,<10.1", "device_detector>=5.0,<6", "satosa>=8.4,<8.6", - "jinja2>=3.0,<4" + "jinja2>=3.0,<4", + "pymongo>=4.4.1,<4.5" ], } )