diff --git a/Dockerfile b/Dockerfile index 88eb14e..1d70ac1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,8 @@ USER user COPY optimizerapi/ /code +ENV FLASK_ENV=production + ENV PATH=/opt/venv/bin:${PATH} CMD [ "python", "./server.py" ] \ No newline at end of file diff --git a/README.md b/README.md index ea51886..af771c2 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,30 @@ Alternatively the project can be build and run with the following commands: python optimizerapi/server.py Now open [http://localhost:9090/v1.0/ui/](http://localhost:9090/v1.0/ui/) in a browser to explore the API through Swagger UI - # Running tests Unit tetsts are located in the "tests" folder and can be run witht the following command python -m pytest + +# Building docker container + + docker build -t process-optimizer-api . +# Obtain encryption key + +Run server once and extract a fresh encryption key from the logs. + + python optimizerapi/server.py + +or using docker + + docker run --rm -it process-optimizer-api +# Running in production + +Running using python + + FLASK_ENV=production PICKLE_KEY= python optimizerapi/server.py + +or use docker + + docker run -d --name process-optimizer-api --env PICKLE_KEY= -p 9090:9090 process-optimizer-api:latest diff --git a/optimizerapi/openapi/specification.yml b/optimizerapi/openapi/specification.yml index bac7a98..04d1e5c 100644 --- a/optimizerapi/openapi/specification.yml +++ b/optimizerapi/openapi/specification.yml @@ -97,6 +97,8 @@ components: result: type: object properties: + pickled: + type: string next: type: array items: diff --git a/optimizerapi/optimizer.py b/optimizerapi/optimizer.py index ba0aa4d..adf401b 100644 --- a/optimizerapi/optimizer.py +++ b/optimizerapi/optimizer.py @@ -1,5 +1,3 @@ -import codecs -import pickle import json from json_tricks import dumps from ProcessOptimizer import Optimizer, expected_minimum @@ -10,6 +8,8 @@ import base64 import io from numbers import Number +from securepickle import pickleToString, unpickleFromString, get_crypto + import numpy numpy.random.seed(42) """ProcessOptimizer web request handler @@ -110,9 +110,10 @@ def processResult(result, optimizer, dimensions, cfg, data, space): addPlot(response["plots"], "convergence") plot_objective(result, dimensions=dimensions, usepartialdependence=False) - addPlot(response["plots"], "objective", debug=True) + addPlot(response["plots"], "objective") - print(str(response)) + prettyResult["pickled"] = pickleToString(result, get_crypto()) + # print(str(response)) return response def addPlot(result, id="generic", close=True, debug=False): diff --git a/optimizerapi/securepickle/__init__.py b/optimizerapi/securepickle/__init__.py new file mode 100644 index 0000000..b59a9a9 --- /dev/null +++ b/optimizerapi/securepickle/__init__.py @@ -0,0 +1,2 @@ +from .pickler import pickleToString, unpickleFromString +from .secure import get_crypto \ No newline at end of file diff --git a/optimizerapi/securepickle/pickler.py b/optimizerapi/securepickle/pickler.py new file mode 100644 index 0000000..ad029c7 --- /dev/null +++ b/optimizerapi/securepickle/pickler.py @@ -0,0 +1,11 @@ +import codecs +import pickle + +def pickleToString(obj, crypto): + pickled = codecs.encode(crypto.encrypt(pickle.dumps(obj)), "base64").decode() + return pickled + +def unpickleFromString(pickled, crypto): + unpickled = pickle.loads(crypto.decrypt(codecs.decode(pickled.encode(), "base64"))) + return unpickled + diff --git a/optimizerapi/securepickle/secure.py b/optimizerapi/securepickle/secure.py new file mode 100644 index 0000000..c9aee33 --- /dev/null +++ b/optimizerapi/securepickle/secure.py @@ -0,0 +1,11 @@ +from cryptography.fernet import Fernet +import os + +def get_crypto(key=None): + if key == None: key = os.getenv("PICKLE_KEY", None) + if key == None: + print("No key found, generating new key") + key = Fernet.generate_key() + os.environ["PICKLE_KEY"] = key.decode("utf-8") + print("To reuse key for future server runs, set environment variable PICKLE_KEY=" + os.environ["PICKLE_KEY"]) + return Fernet(key) \ No newline at end of file diff --git a/optimizerapi/server.py b/optimizerapi/server.py index fd5fcae..f46f7ab 100644 --- a/optimizerapi/server.py +++ b/optimizerapi/server.py @@ -1,6 +1,15 @@ +import os import connexion +from securepickle import get_crypto +from waitress import serve if __name__ == '__main__': + # Initialize crypto + get_crypto() app = connexion.FlaskApp(__name__, port=9090, specification_dir='./openapi/') app.add_api('specification.yml', arguments={'title': 'Hello World Example'}) - app.run() \ No newline at end of file + if os.getenv("FLASK_ENV", "development") == "development": + os.environ["FLASK_ENV"] = "development" + app.run() + else: + serve(app, listen='*:9090') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 32b37b6..0ea2dba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ connexion==2.7.0 connexion[swagger-ui] Flask==1.1.2 ProcessOptimizer==0.6.1 -json-tricks==3.15.5 \ No newline at end of file +json-tricks==3.15.5 +cryptography==3.4.7 +waitress==2.0.0 \ No newline at end of file diff --git a/tests/test_securepickle.py b/tests/test_securepickle.py new file mode 100644 index 0000000..c367fc5 --- /dev/null +++ b/tests/test_securepickle.py @@ -0,0 +1,15 @@ +from optimizerapi.securepickle import pickleToString, unpickleFromString, get_crypto + +def test_pickleToString(): + f = get_crypto() + encoded = pickleToString("myString", f) + assert encoded != "myString" + +def test_unpickleFromString(): + f = get_crypto() + encoded = pickleToString("myString", f) + decoded = unpickleFromString(encoded, f) + assert decoded == "myString" + + +