diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index b94a7f0..a52dedf 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ $ flask --version # Werkzeug ``` +Note: You can use either `python3` or `python`, `pip3` or `pip` in the commands above. + If you have never developed a Flask application before, high chance you haven't had it yet. The installation is quite simple ```bash @@ -84,16 +86,30 @@ You can manually install each of them or run: $ pipenv install --dev ``` +Note: You might need to specify the path to your Python installation. If such an error message arises, try running: +```bash +$ pipenv --python path/to/python install --dev +``` +where `path/to/python` is the path to your Python installation. Depending on your Python version, this may or may not fail (Please try proceeding to the next steps, a warning or error message from `pipenv` does not necessarily imply failure). In that case, consider downloading Python version `3.9.10` specifically. + ## Running locally To run this app on your local server, direct to the `cashman-flask-project` folder to facilitate the start up of our application: +#### For MacOS: ```bash $ cd cashman-flask-project $ chmod +x bootstrap.sh $ ./bootstrap.sh ``` +#### For Windows (Use [Git Bash](https://git-scm.com/downloads)): +Note: If you are using `python` instead of `python3`, edit line 5 of the `bootstrap.sh` file to `source $(python -m pipenv --venv)/bin/activate` before running the commands above. +```bash +$ cd cashman-flask project +$ sh bootstrap.sh +``` + This will first defines the main script (`index.py`) to be executed by Flask, then activate a virtual environment by `pipenv` that locates exact versions of our dependencies, then run our Flask app. If the script is working properly, you should expect to see a result similar to this: @@ -387,6 +403,9 @@ Unable to deserialize: %Signature has expired By default, the tokens from https://demo.scitokens.org/ are valid for 10 minutes, so you may see this error if you use the same token for over 10 minutes. In that case, you can get a new token from https://demo.scitokens.org/ and try again. +## Developer Notes: +For building a Docker image of the REST Demo App, first visit `index.py` and follow the instructions at the top. Then, create a Docker Image normally. + ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. diff --git a/cashman-flask-project/.env b/cashman-flask-project/.env new file mode 100644 index 0000000..cdb9543 --- /dev/null +++ b/cashman-flask-project/.env @@ -0,0 +1,2 @@ +## The port on which to expose the web application. +WEBAPP_PORT=9801 diff --git a/cashman-flask-project/.gitignore b/cashman-flask-project/.gitignore index fe33a8e..992124a 100644 --- a/cashman-flask-project/.gitignore +++ b/cashman-flask-project/.gitignore @@ -112,7 +112,6 @@ celerybeat.pid *.sage.py # Environments -.env .venv env/ venv/ diff --git a/cashman-flask-project/Dockerfile b/cashman-flask-project/Dockerfile new file mode 100644 index 0000000..9837fb9 --- /dev/null +++ b/cashman-flask-project/Dockerfile @@ -0,0 +1,46 @@ +FROM hub.opensciencegrid.org/opensciencegrid/software-base:3.6-el8-release + + +## Set the Python version. + + +ARG PY_PKG=python39 +ARG PY_EXE=python3.9 + + +## Locale and Python settings required by Flask. + + +ENV LANG="en_US.utf8" +ENV LC_ALL="en_US.utf8" +ENV PYTHONUNBUFFERED=1 + + +## Install core dependencies and configuration. + + +RUN yum module enable -y ${PY_PKG} \ + && yum update -y \ + && yum install -y httpd mod_ssl ${PY_PKG}-pip ${PY_PKG}-mod_wsgi \ + && yum clean all \ + && rm -rf /etc/httpd/conf.d/* /var/cache/yum/ \ + # + && ${PY_EXE} -m pip install --no-cache-dir -U pip setuptools wheel + +COPY etc /etc/ + + +## Install the Flask and WSGI applications. + +# Cashman +COPY cashman /srv/cashman +COPY utils /srv/utils + +# Dependencies +COPY requirements.txt Pipfile Pipfile.lock /srv/ + +# Main WSGI app +COPY wsgi.py /srv/ + +WORKDIR /srv +RUN ${PY_EXE} -m pip install --no-cache-dir -r requirements.txt \ No newline at end of file diff --git a/cashman-flask-project/bootstrap.sh b/cashman-flask-project/bootstrap.sh index 07fe107..cd44b4f 100755 --- a/cashman-flask-project/bootstrap.sh +++ b/cashman-flask-project/bootstrap.sh @@ -2,5 +2,5 @@ export FLASK_APP=./cashman/index.py export AUTH0_DOMAIN=dev-7jgsf20u.us.auth0.com export API_IDENTIFIER="https://cashman/api" -source $(python3 -m pipenv --venv)/bin/activate +source $(python3 -m pipenv --venv) /bin/activate flask run diff --git a/cashman-flask-project/cashman/__init__.py b/cashman-flask-project/cashman/__init__.py index e69de29..24ac1d1 100644 --- a/cashman-flask-project/cashman/__init__.py +++ b/cashman-flask-project/cashman/__init__.py @@ -0,0 +1,24 @@ +# import os +# from flask import Flask + +# def create_app(cfg = None): +# # Create app +# app = Flask(__name__) + +# load_config(app, cfg) + +# # import all route modules +# # and register blueprints ??? + +# return app + +# def load_config(app, cfg): +# # Load a default configuration file +# app.config.from_pyfile('config/default.cfg') + +# # If cfg is empty try to load config file from environment variable +# if cfg is None and 'YOURAPPLICATION_CFG' in os.environ: +# cfg = os.environ['YOURAPPLICATION_CFG'] + +# if cfg is not None: +# app.config.from_pyfile(cfg) \ No newline at end of file diff --git a/cashman-flask-project/cashman/index.py b/cashman-flask-project/cashman/index.py index a7c4ec0..b3e774b 100644 --- a/cashman-flask-project/cashman/index.py +++ b/cashman-flask-project/cashman/index.py @@ -1,11 +1,15 @@ """Python Flask RESTful APIs with Auth0 & SciAuth integration """ -from flask import Flask, jsonify, request +from flask import Flask, jsonify, request, Blueprint from flask_cors import cross_origin from utils.auth0_decorator import requires_auth, requires_scope from utils.AuthError import AuthError from utils.scitokens_protect import protect +# NOTE: Toggle between the following two `app = ...` lines for different purposes: +## - Blueprint(...): Building a Docker image +## - Flask(...): Running the demo app directly according to the documentation +# app = Blueprint("app_bp", __name__) app = Flask(__name__) # Create sample data diff --git a/cashman-flask-project/docker-compose.yaml b/cashman-flask-project/docker-compose.yaml new file mode 100644 index 0000000..78ebbe6 --- /dev/null +++ b/cashman-flask-project/docker-compose.yaml @@ -0,0 +1,36 @@ +--- +version: "3.0" + +services: + + webapp: + + build: + context: . + dockerfile: Dockerfile + + image: webapp:dev + container_name: webapp + restart: always + + # Expose the container's web server on a non-standard port so that + # it can coexist on the host with other web servers. + + ports: + - ${WEBAPP_PORT}:8443 + + secrets: + - source: httpd-conf + target: /etc/httpd/conf.d/httpd.conf + - source: tls-crt + target: /certs/tls.crt + - source: tls-key + target: /certs/tls.key + +secrets: + httpd-conf: + file: secrets/httpd.conf + tls-crt: + file: secrets/tls.crt + tls-key: + file: secrets/tls.key diff --git a/cashman-flask-project/etc/httpd/conf.d/httpd.conf b/cashman-flask-project/etc/httpd/conf.d/httpd.conf new file mode 100644 index 0000000..e1829db --- /dev/null +++ b/cashman-flask-project/etc/httpd/conf.d/httpd.conf @@ -0,0 +1,59 @@ +# +# Configuration for a web application that uses: +# +# - mod_ssl +# - mod_wsgi +# +# Features: +# +# - Listens for SSL connections on port 8443. +# - Routes all requests through the web application. +# + +ServerName webapp.localdomain +Listen 8443 + +## Minimize information sent about this server. + +ServerSignature Off +ServerTokens ProductOnly +TraceEnable Off + + + ServerName webapp.localdomain + ServerAdmin someone@example.com + + ## Deny access to the file system. + + + Require all denied + Options None + AllowOverride None + + + ## Do not restrict the web space. + + + Require all granted + AuthType none + + + ## Configure logging. + + ErrorLog "/var/log/httpd/local_default_ssl_error_ssl.log" + LogLevel info + CustomLog "/var/log/httpd/local_default_ssl_access_ssl.log" combined + + ## Configure SSL. + + SSLEngine on + SSLCertificateFile "/certs/tls.crt" + SSLCertificateKeyFile "/certs/tls.key" + SSLCertificateChainFile "/certs/tls.crt" + + ## Configure WSGI. + + WSGIDaemonProcess WebApp display-name=WebApp processes=2 home=/srv + WSGIProcessGroup WebApp + WSGIScriptAlias / "/srv/wsgi.py" + diff --git a/cashman-flask-project/etc/supervisord.d/httpd.conf b/cashman-flask-project/etc/supervisord.d/httpd.conf new file mode 100644 index 0000000..35a8bc6 --- /dev/null +++ b/cashman-flask-project/etc/supervisord.d/httpd.conf @@ -0,0 +1,3 @@ +[program:httpd] +command=/bin/bash -c "exec /usr/sbin/httpd $OPTIONS -DFOREGROUND" +autorestart=true diff --git a/cashman-flask-project/requirements.txt b/cashman-flask-project/requirements.txt index c7258e7..5ad24e8 100644 --- a/cashman-flask-project/requirements.txt +++ b/cashman-flask-project/requirements.txt @@ -2,4 +2,5 @@ flask python-dotenv python-jose flask-cors -six \ No newline at end of file +six +scitokens \ No newline at end of file diff --git a/cashman-flask-project/secrets/tls.key b/cashman-flask-project/secrets/tls.key new file mode 100644 index 0000000..e69de29 diff --git a/cashman-flask-project/utils/auth0_decorator.py b/cashman-flask-project/utils/auth0_decorator.py index 761300e..8c61324 100644 --- a/cashman-flask-project/utils/auth0_decorator.py +++ b/cashman-flask-project/utils/auth0_decorator.py @@ -3,7 +3,7 @@ import json from os import environ as env -from six.moves.urllib.request import urlopen +import six from dotenv import load_dotenv, find_dotenv from flask import request, _request_ctx_stack @@ -64,7 +64,7 @@ def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): token = get_token_auth_header() - jsonurl = urlopen("https://" + AUTH0_DOMAIN + "/.well-known/jwks.json") + jsonurl = six.moves.urllib.request.urlopen("https://" + AUTH0_DOMAIN + "/.well-known/jwks.json") jwks = json.loads(jsonurl.read()) unverified_header = jwt.get_unverified_header(token) rsa_key = {} diff --git a/cashman-flask-project/utils/scitokens_protect.py b/cashman-flask-project/utils/scitokens_protect.py index 9a9bd20..4245cc4 100644 --- a/cashman-flask-project/utils/scitokens_protect.py +++ b/cashman-flask-project/utils/scitokens_protect.py @@ -4,9 +4,8 @@ import traceback import inspect -audience = "https://demo.scitokens.org" -issuers = ["https://demo.scitokens.org"] - +audiences = ["https://demo.scitokens.org", "https://token-issuer.localdomain"] +issuers = ["https://demo.scitokens.org", "https://token-issuer.localdomain"] def protect(**outer_kwargs): def real_decorator(some_function): @@ -22,13 +21,30 @@ def wrapper(*args, **kwargs): return ("Authentication header incorrect format", 401, headers) serialized_token = bearer.split()[1] - try: - token = scitokens.SciToken.deserialize(serialized_token, audience) - except Exception as e: - print(str(e)) - traceback.print_exc() + myAudience = None + parsedToken = None + exception = None + exeTrace = "" + for audience in audiences: + try: + # method 1: insecure -> True + # method 2: public_key -> Get public key and test in deserialize + # + normally would be fetched from token issuer, but just code in + # In configuration for demo, check public_key as well + parsedToken = scitokens.SciToken.deserialize(serialized_token, audience) + myAudience = audience + break + except Exception as e: + exception = e + exeTrace = traceback.format_exc() + continue + + # Check if found valid audience + if not myAudience: + print(str(exception)) + traceback.print_exc() # TODO: Not too sure how to handle this. Maybe format_exc? headers = {"WWW-Authenticate": "Bearer"} - return ("Unable to deserialize: %{}".format(str(e)), 401, headers) + return ("Unable to deserialize: %{}".format(str(exception)), 401, headers) # if not isinstance(issuers, list): # issuers = [issuers] @@ -36,8 +52,8 @@ def wrapper(*args, **kwargs): permission = outer_kwargs["permission"] path = request.path for issuer in issuers: - enforcer = scitokens.Enforcer(issuer, audience) - if enforcer.test(token, permission, path): + enforcer = scitokens.Enforcer(issuer, myAudience) + if enforcer.test(parsedToken, permission, path): success = True break @@ -51,7 +67,7 @@ def wrapper(*args, **kwargs): # If the function takes "token" as an argument, send the token if "token" in inspect.getfullargspec(some_function).args: - kwargs["token"] = token + kwargs["token"] = parsedToken return some_function(*args, **kwargs) diff --git a/cashman-flask-project/wsgi.py b/cashman-flask-project/wsgi.py new file mode 100755 index 0000000..86a9fb4 --- /dev/null +++ b/cashman-flask-project/wsgi.py @@ -0,0 +1,23 @@ +from flask import Flask + +from cashman import index + +def load_config(app: Flask) -> None: + pass + +def register_blueprints(app: Flask) -> None: + app.register_blueprint(index.app) + + +def create_app() -> Flask: + app = Flask( + __name__.split(".", maxsplit=1)[0], + ) + + load_config(app) + register_blueprints(app) + + return app + + +application = create_app() \ No newline at end of file