Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.DS_Store
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions cashman-flask-project/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
## The port on which to expose the web application.
WEBAPP_PORT=9801
1 change: 0 additions & 1 deletion cashman-flask-project/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ celerybeat.pid
*.sage.py

# Environments
.env
.venv
env/
venv/
Expand Down
46 changes: 46 additions & 0 deletions cashman-flask-project/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion cashman-flask-project/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 24 additions & 0 deletions cashman-flask-project/cashman/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 5 additions & 1 deletion cashman-flask-project/cashman/index.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
36 changes: 36 additions & 0 deletions cashman-flask-project/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions cashman-flask-project/etc/httpd/conf.d/httpd.conf
Original file line number Diff line number Diff line change
@@ -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

<VirtualHost *:8443>
ServerName webapp.localdomain
ServerAdmin [email protected]

## Deny access to the file system.

<Directory "/">
Require all denied
Options None
AllowOverride None
</Directory>

## Do not restrict the web space.

<Location "/">
Require all granted
AuthType none
</Location>

## 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"
</VirtualHost>
3 changes: 3 additions & 0 deletions cashman-flask-project/etc/supervisord.d/httpd.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[program:httpd]
command=/bin/bash -c "exec /usr/sbin/httpd $OPTIONS -DFOREGROUND"
autorestart=true
3 changes: 2 additions & 1 deletion cashman-flask-project/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ flask
python-dotenv
python-jose
flask-cors
six
six
scitokens
Empty file.
4 changes: 2 additions & 2 deletions cashman-flask-project/utils/auth0_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {}
Expand Down
40 changes: 28 additions & 12 deletions cashman-flask-project/utils/scitokens_protect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -22,22 +21,39 @@ 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]
success = False
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

Expand All @@ -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)

Expand Down
23 changes: 23 additions & 0 deletions cashman-flask-project/wsgi.py
Original file line number Diff line number Diff line change
@@ -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()