Reorder config imports so that instance config is dead last - and can override everything else.

Updated docker-compose for running a demo.
run_pyl fixes
This commit is contained in:
Dan 2022-12-01 14:12:25 -05:00
parent 8a21450ff7
commit 186727e371
11 changed files with 123 additions and 105 deletions

View File

@ -6,46 +6,46 @@ services:
platform: linux/amd64
cap_add:
- SYS_NICE
restart: "${SPIFFWORKFLOW_BACKEND_DATABASE_DOCKER_RESTART_POLICY:-no}"
restart: "no"
environment:
- MYSQL_DATABASE=${SPIFFWORKFLOW_BACKEND_DATABASE_NAME:-spiffworkflow_backend_development}
- MYSQL_ROOT_PASSWORD=${SPIFFWORKFLOW_BACKEND_MYSQL_ROOT_DATABASE:-my-secret-pw}
- MYSQL_DATABASE=spiffworkflow_backend_development
- MYSQL_ROOT_PASSWORD=my-secret-pw
- MYSQL_TCP_PORT=7003
ports:
- "7003"
volumes:
- spiffworkflow_backend:/var/lib/mysql
healthcheck:
test: mysql --user=root --password=${SPIFFWORKFLOW_BACKEND_MYSQL_ROOT_DATABASE:-my-secret-pw} -e 'select 1' ${SPIFFWORKFLOW_BACKEND_DATABASE_NAME:-spiffworkflow_backend_development}
test: mysql --user=root --password=my-secret-pw -e 'select 1' spiffworkflow_backend_development
interval: 10s
timeout: 5s
retries: 10
spiffworkflow-backend:
container_name: spiffworkflow-backend
image: ghcr.io/sartography/spiffworkflow-backend
image: ghcr.io/sartography/spiffworkflow-backend:latest
depends_on:
spiffworkflow-db:
condition: service_healthy
environment:
- APPLICATION_ROOT=/
- SPIFFWORKFLOW_BACKEND_ENV=${SPIFFWORKFLOW_BACKEND_ENV:-development}
- SPIFFWORKFLOW_BACKEND_ENV=development
- FLASK_DEBUG=0
- FLASK_SESSION_SECRET_KEY=${FLASK_SESSION_SECRET_KEY:-super_secret_key}
- OPEN_ID_SERVER_URL=${OPEN_ID_SERVER_URL:-http://spiffworkflow-openid:7002}
- SPIFFWORKFLOW_FRONTEND_URL=${SPIFFWORKFLOW_FRONTEND_URL:-http://spiffworkflow-frontend:7001}
- SPIFFWORKFLOW_BACKEND_URL=${SPIFFWORKFLOW_BACKEND_URL:-http://spiffworkflow-backend:7000}
- FLASK_SESSION_SECRET_KEY=super_secret_key
- OPEN_ID_SERVER_URL=http://localhost:7000/openid
- SPIFFWORKFLOW_FRONTEND_URL=http://localhost:7001
- SPIFFWORKFLOW_BACKEND_URL=http://localhost:7000
- SPIFFWORKFLOW_BACKEND_PORT=7000
- SPIFFWORKFLOW_BACKEND_UPGRADE_DB=true
- SPIFFWORKFLOW_BACKEND_DATABASE_URI=mysql+mysqlconnector://root:${SPIFFWORKFLOW_BACKEND_MYSQL_ROOT_DATABASE:-my-secret-pw}@spiffworkflow-db:7003/${SPIFFWORKFLOW_BACKEND_DATABASE_NAME:-spiffworkflow_backend_development}
- SPIFFWORKFLOW_BACKEND_DATABASE_URI=mysql+mysqlconnector://root:my-secret-pw@spiffworkflow-db:7003/spiffworkflow_backend_development
- BPMN_SPEC_ABSOLUTE_DIR=/app/process_models
- SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA=${SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA:-false}
- SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME=${SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME:-acceptance_tests.yml}
- SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA=false
- SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME=demo.yml
- RUN_BACKGROUND_SCHEDULER=true
- OPEN_ID_CLIENT_ID=spiffworkflow-backend
- OPEN_ID_CLIENT_SECRET_KEY=my_open_id_secret_key
ports:
- "7000:7000"
volumes:
- ${BPMN_SPEC_ABSOLUTE_DIR:-./../sample-process-models}:/app/process_models
- ./process_models:/app/process_models
- ./log:/app/log
healthcheck:
test: curl localhost:7000/v1.0/status --fail

View File

@ -19,7 +19,9 @@ from werkzeug.exceptions import NotFound
import spiffworkflow_backend.load_database_models # noqa: F401
from spiffworkflow_backend.config import setup_config
from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import openid_blueprint
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import (
openid_blueprint,
)
from spiffworkflow_backend.routes.process_api_blueprint import process_api_blueprint
from spiffworkflow_backend.routes.user import verify_token
from spiffworkflow_backend.routes.user_blueprint import user_blueprint

View File

@ -52,12 +52,6 @@ def setup_config(app: Flask) -> None:
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config.from_object("spiffworkflow_backend.config.default")
# This allows config/testing.py or instance/config.py to override the default config
if "ENV_IDENTIFIER" in app.config and app.config["ENV_IDENTIFIER"] == "testing":
app.config.from_pyfile("config/testing.py", silent=True)
else:
app.config.from_pyfile(f"{app.instance_path}/config.py", silent=True)
env_config_prefix = "spiffworkflow_backend.config."
env_config_module = env_config_prefix + app.config["ENV_IDENTIFIER"]
try:
@ -73,6 +67,12 @@ def setup_config(app: Flask) -> None:
f"Cannot find config module: {env_config_module}"
) from exception
# This allows config/testing.py or instance/config.py to override the default config
if "ENV_IDENTIFIER" in app.config and app.config["ENV_IDENTIFIER"] == "testing":
app.config.from_pyfile("config/testing.py", silent=True)
else:
app.config.from_pyfile(f"{app.instance_path}/config.py", silent=True)
setup_database_uri(app)
setup_logger(app)

View File

@ -30,7 +30,11 @@ CONNECTOR_PROXY_URL = environ.get(
GIT_COMMIT_ON_SAVE = environ.get("GIT_COMMIT_ON_SAVE", default="false") == "true"
# Open ID server
OPEN_ID_SERVER_URL = environ.get("OPEN_ID_SERVER_URL", default="http://localhost:7002/realms/spiffworkflow")
OPEN_ID_SERVER_URL = environ.get(
"OPEN_ID_SERVER_URL", default="http://localhost:7002/realms/spiffworkflow"
)
# Replace above line with this to use the built-in Open ID Server.
# OPEN_ID_SERVER_URL = environ.get("OPEN_ID_SERVER_URL", default="http://localhost:7000/openid")
OPEN_ID_CLIENT_ID = environ.get("OPEN_ID_CLIENT_ID", default="spiffworkflow-backend")
OPEN_ID_CLIENT_SECRET_KEY = environ.get(
"OPEN_ID_CLIENT_SECRET_KEY", default="JXeQExm0JhQPLumgHtIIqf52bDalHz0q"

View File

@ -30,7 +30,8 @@ class UserModel(SpiffworkflowBaseDBModel):
__table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),)
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(255), nullable=False, unique=False) # server and service id must be unique, not username.
# server and service id must be unique, not username.
username = db.Column(db.String(255), nullable=False, unique=False)
uid = db.Column(db.String(50), unique=True)
service = db.Column(db.String(50), nullable=False, unique=False)
service_id = db.Column(db.String(255), nullable=False, unique=False)

View File

@ -9,7 +9,12 @@ from urllib.parse import urlencode
import jwt
import yaml
from flask import Blueprint, render_template, request, current_app, redirect, url_for
from flask import Blueprint
from flask import current_app
from flask import redirect
from flask import render_template
from flask import request
from flask import url_for
openid_blueprint = Blueprint(
"openid", __name__, template_folder="templates", static_folder="static"
@ -21,8 +26,8 @@ MY_SECRET_CODE = ":this_is_not_secure_do_not_use_in_production"
@openid_blueprint.route("/.well-known/openid-configuration", methods=["GET"])
def well_known():
"""OpenID Discovery endpoint -- as these urls can be very different from system to system,
this is just a small subset."""
host_url = request.host_url.strip('/')
this is just a small subset."""
host_url = request.host_url.strip("/")
return {
"issuer": f"{host_url}/openid",
"authorization_endpoint": f"{host_url}{url_for('openid.auth')}",
@ -34,82 +39,90 @@ def well_known():
@openid_blueprint.route("/auth", methods=["GET"])
def auth():
"""Accepts a series of parameters"""
return render_template('login.html',
state=request.args.get('state'),
response_type=request.args.get('response_type'),
client_id=request.args.get('client_id'),
scope=request.args.get('scope'),
redirect_uri=request.args.get('redirect_uri'),
error_message=request.args.get('error_message', ''))
return render_template(
"login.html",
state=request.args.get("state"),
response_type=request.args.get("response_type"),
client_id=request.args.get("client_id"),
scope=request.args.get("scope"),
redirect_uri=request.args.get("redirect_uri"),
error_message=request.args.get("error_message", ""),
)
@openid_blueprint.route("/form_submit", methods=["POST"])
def form_submit():
users = get_users()
if request.values['Uname'] in users and request.values['Pass'] == users[request.values['Uname']]["password"]:
if (
request.values["Uname"] in users
and request.values["Pass"] == users[request.values["Uname"]]["password"]
):
# Redirect back to the end user with some detailed information
state = request.values.get('state')
state = request.values.get("state")
data = {
"state": state,
"code": request.values['Uname'] + MY_SECRET_CODE,
"session_state": ""
"code": request.values["Uname"] + MY_SECRET_CODE,
"session_state": "",
}
url = request.values.get('redirect_uri') + "?" + urlencode(data)
url = request.values.get("redirect_uri") + "?" + urlencode(data)
return redirect(url)
else:
return render_template('login.html',
state=request.values.get('state'),
response_type=request.values.get('response_type'),
client_id=request.values.get('client_id'),
scope=request.values.get('scope'),
redirect_uri=request.values.get('redirect_uri'),
error_message="Login failed. Please try again.")
return render_template(
"login.html",
state=request.values.get("state"),
response_type=request.values.get("response_type"),
client_id=request.values.get("client_id"),
scope=request.values.get("scope"),
redirect_uri=request.values.get("redirect_uri"),
error_message="Login failed. Please try again.",
)
@openid_blueprint.route("/token", methods=["POST"])
def token():
"""Url that will return a valid token, given the super secret sauce"""
grant_type = request.values.get('grant_type')
code = request.values.get('code')
redirect_uri = request.values.get('redirect_uri')
request.values.get("grant_type")
code = request.values.get("code")
request.values.get("redirect_uri")
"""We just stuffed the user name on the front of the code, so grab it."""
user_name, secret_hash = code.split(":")
user_details = get_users()[user_name]
"""Get authentication from headers."""
authorization = request.headers.get('Authorization')
authorization = request.headers.get("Authorization")
authorization = authorization[6:] # Remove "Basic"
authorization = base64.b64decode(authorization).decode('utf-8')
authorization = base64.b64decode(authorization).decode("utf-8")
client_id, client_secret = authorization.split(":")
base_url = request.host_url + "openid"
access_token = user_name + ":" + "always_good_demo_access_token"
refresh_token = user_name + ":" + "always_good_demo_refresh_token"
id_token = jwt.encode({
"iss": base_url,
"aud": [client_id, "account"],
"iat": time.time(),
"exp": time.time() + 86400, # Expire after a day.
"sub": user_name,
"preferred_username": user_details.get('preferred_username', user_name)
},
id_token = jwt.encode(
{
"iss": base_url,
"aud": [client_id, "account"],
"iat": time.time(),
"exp": time.time() + 86400, # Expire after a day.
"sub": user_name,
"preferred_username": user_details.get("preferred_username", user_name),
},
client_secret,
algorithm="HS256",
)
response = {
"access_token": id_token,
"id_token": id_token,
"refresh_token": id_token
"refresh_token": id_token,
}
return response
@openid_blueprint.route("/end_session", methods=["GET"])
def end_session():
redirect_url = request.args.get('post_logout_redirect_uri')
id_token_hint = request.args.get('id_token_hint')
redirect_url = request.args.get("post_logout_redirect_uri")
request.args.get("id_token_hint")
return redirect(redirect_url)

View File

@ -33,4 +33,4 @@
</form>
</div>
</body>
</html>
</html>

View File

@ -15,12 +15,9 @@ from flask import request
from flask_bpmn.api.api_error import ApiError
from werkzeug.wrappers import Response
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel
from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.authentication_service import (
AuthenticationService, AuthenticationProviderTypes,
AuthenticationService,
)
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.user_service import UserService
@ -91,7 +88,7 @@ def verify_token(
if auth_token and "error" not in auth_token:
# We have the user, but this code is a bit convoluted, and will later demand
# a user_info object so it can look up the user. Sorry to leave this crap here.
user_info = {"sub": user.service_id }
user_info = {"sub": user.service_id}
else:
raise ae
else:
@ -200,16 +197,18 @@ def login(redirect_url: str = "/") -> Response:
)
return redirect(login_redirect_url)
def parse_id_token(token: str) -> dict:
parts = token.split(".")
if len(parts) != 3:
raise Exception("Incorrect id token format")
payload = parts[1]
padded = payload + '=' * (4 - len(payload) % 4)
padded = payload + "=" * (4 - len(payload) % 4)
decoded = base64.b64decode(padded)
return json.loads(decoded)
def login_return(code: str, state: str, session_state: str) -> Optional[Response]:
"""Login_return."""
state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8"))

View File

@ -3,7 +3,6 @@ import base64
import enum
import json
import time
import typing
from typing import Optional
import jwt
@ -26,7 +25,10 @@ class AuthenticationProviderTypes(enum.Enum):
class AuthenticationService:
"""AuthenticationService."""
ENDPOINT_CACHE = {} # We only need to find the openid endpoints once, then we can cache them.
ENDPOINT_CACHE = (
{}
) # We only need to find the openid endpoints once, then we can cache them.
@staticmethod
def client_id():
@ -40,7 +42,6 @@ class AuthenticationService:
def secret_key():
return current_app.config["OPEN_ID_CLIENT_SECRET_KEY"]
@classmethod
def open_id_endpoint_for_name(cls, name: str) -> None:
"""All openid systems provide a mapping of static names to the full path of that endpoint."""

View File

@ -6,9 +6,10 @@ from typing import Union
import jwt
import yaml
from flask import current_app, scaffold
from flask import current_app
from flask import g
from flask import request
from flask import scaffold
from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db
from SpiffWorkflow.task import Task as SpiffTask # type: ignore
@ -127,7 +128,6 @@ class AuthorizationService:
db.session.add(user_group_assignemnt)
db.session.commit()
@classmethod
def import_permissions_from_yaml_file(
cls, raise_if_missing_user: bool = False
@ -448,7 +448,6 @@ class AuthorizationService:
email=email,
)
# this may eventually get too slow.
# when it does, be careful about backgrounding, because
# the user will immediately need permissions to use the site.

View File

@ -1,59 +1,58 @@
"""Test_authentication."""
import ast
import base64
from flask import Flask
from flask.testing import FlaskClient
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from spiffworkflow_backend.services.authentication_service import (
AuthenticationService,
)
class TestFaskOpenId(BaseTest):
"""An integrated Open ID that responds to openID requests
by referencing a build in YAML file. Useful for
local development, testing, demos etc..."""
by referencing a build in YAML file. Useful for
local development, testing, demos etc..."""
def test_discovery_of_endpoints(self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,) -> None:
def test_discovery_of_endpoints(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
response = client.get("/openid/.well-known/openid-configuration")
discovered_urls = response.json
assert "http://localhost/openid" == discovered_urls["issuer"]
assert "http://localhost/openid/auth" == discovered_urls["authorization_endpoint"]
assert (
"http://localhost/openid/auth" == discovered_urls["authorization_endpoint"]
)
assert "http://localhost/openid/token" == discovered_urls["token_endpoint"]
def test_get_login_page(self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,) -> None:
def test_get_login_page(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
# It should be possible to get to a login page
data = {
"state": {"bubblegum":1, "daydream":2}
}
data = {"state": {"bubblegum": 1, "daydream": 2}}
response = client.get("/openid/auth", query_string=data)
assert b"<h2>Login to SpiffWorkflow</h2>" in response.data
assert b"bubblegum" in response.data
def test_get_token(self,
def test_get_token(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,) -> None:
with_db_and_bpmn_file_cleanup: None,
) -> None:
code = "c3BpZmZ3b3JrZmxvdy1iYWNrZW5kOkpYZVFFeG0wSmhRUEx1bWdIdElJcWY1MmJEYWxIejBx"
code = (
"c3BpZmZ3b3JrZmxvdy1iYWNrZW5kOkpYZVFFeG0wSmhRUEx1bWdIdElJcWY1MmJEYWxIejBx"
)
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Basic {code}",
}
data = {
"grant_type": 'authorization_code',
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_url": 'http://localhost:7000/v1.0/login_return'
"redirect_url": "http://localhost:7000/v1.0/login_return",
}
response = client.post("/openid/token", data=data, headers=headers)
assert response