Merge branch 'main' into feature/waku-fault-message

This commit is contained in:
mike cullerton 2023-01-13 08:29:42 -05:00
commit ff48f67918
31 changed files with 292 additions and 290 deletions

View File

@ -9,7 +9,6 @@ def main() -> None:
"""Main."""
app = create_app()
with app.app_context():
print("HEY")
failing_process_models = DataSetupService.save_all_process_models()
for bpmn_errors in failing_process_models:
print(bpmn_errors)

View File

@ -39,7 +39,8 @@ docker run \
-e KEYCLOAK_LOGLEVEL=ALL \
-e ROOT_LOGLEVEL=ALL \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:20.0.1 start-dev \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:20.0.1 start-dev \
-Dkeycloak.profile.feature.token_exchange=enabled \
-Dkeycloak.profile.feature.admin_fine_grained_authz=enabled

View File

@ -462,21 +462,6 @@ toml = "*"
conda = ["pyyaml"]
pipenv = ["pipenv"]
[[package]]
name = "ecdsa"
version = "0.18.0"
description = "ECDSA cryptographic signature library (pure python)"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[package.dependencies]
six = ">=1.9.0"
[package.extras]
gmpy = ["gmpy"]
gmpy2 = ["gmpy2"]
[[package]]
name = "exceptiongroup"
version = "1.0.4"
@ -668,6 +653,22 @@ python-versions = "*"
Flask = ">=0.9"
Six = "*"
[[package]]
name = "flask-jwt-extended"
version = "4.4.4"
description = "Extended JWT integration with Flask"
category = "main"
optional = false
python-versions = ">=3.7,<4"
[package.dependencies]
Flask = ">=2.0,<3.0"
PyJWT = ">=2.0,<3.0"
Werkzeug = ">=0.14"
[package.extras]
asymmetric-crypto = ["cryptography (>=3.3.1)"]
[[package]]
name = "Flask-Mail"
version = "0.9.1"
@ -1223,14 +1224,6 @@ category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "pyasn1"
version = "0.4.8"
description = "ASN.1 types and codecs"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pycodestyle"
version = "2.8.0"
@ -1384,41 +1377,6 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
[package.dependencies]
six = ">=1.5"
[[package]]
name = "python-jose"
version = "3.3.0"
description = "JOSE implementation in Python"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
ecdsa = "!=0.15"
pyasn1 = "*"
rsa = "*"
[package.extras]
cryptography = ["cryptography (>=3.4.0)"]
pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"]
pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"]
[[package]]
name = "python-keycloak"
version = "2.6.0"
description = "python-keycloak is a Python package providing access to the Keycloak API."
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
[package.dependencies]
python-jose = ">=3.3.0,<4.0.0"
requests = ">=2.20.0,<3.0.0"
requests-toolbelt = ">=0.9.1,<0.10.0"
urllib3 = ">=1.26.0,<2.0.0"
[package.extras]
docs = ["Sphinx (>=5.0.2,<6.0.0)", "alabaster (>=0.7.12,<0.8.0)", "commonmark (>=0.9.1,<0.10.0)", "m2r2 (>=0.3.2,<0.4.0)", "mock (>=4.0.3,<5.0.0)", "readthedocs-sphinx-ext (>=2.1.8,<3.0.0)", "recommonmark (>=0.7.1,<0.8.0)", "sphinx-autoapi (>=1.8.4,<2.0.0)", "sphinx-rtd-theme (>=1.0.0,<2.0.0)"]
[[package]]
name = "pytz"
version = "2022.6"
@ -1494,17 +1452,6 @@ urllib3 = ">=1.21.1,<1.27"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "requests-toolbelt"
version = "0.9.1"
description = "A utility belt for advanced users of python-requests"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
requests = ">=2.0.1,<3.0.0"
[[package]]
name = "restrictedpython"
version = "6.0"
@ -1528,17 +1475,6 @@ python-versions = "*"
[package.dependencies]
docutils = ">=0.11,<1.0"
[[package]]
name = "rsa"
version = "4.9"
description = "Pure-Python RSA implementation"
category = "main"
optional = false
python-versions = ">=3.6,<4"
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
name = "ruamel.yaml"
version = "0.17.21"
@ -1851,7 +1787,7 @@ lxml = "*"
type = "git"
url = "https://github.com/sartography/SpiffWorkflow"
reference = "main"
resolved_reference = "5eed83ab12f67c01c7836424a22fc425a33fc55d"
resolved_reference = "be26100bcbef8026e26312c665dae42faf476485"
[[package]]
name = "SQLAlchemy"
@ -2222,7 +2158,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "1.1"
python-versions = ">=3.9,<3.12"
content-hash = "832c1b6cd8d9aebc8529fdce11167bddcb3634fd0767dd2e490b74ababcf2714"
content-hash = "8592e94ba80b7d0338a9c003ca4d0e189b5f470d97391438ddc1fc9050febedb"
[metadata.files]
alabaster = [
@ -2443,10 +2379,6 @@ dparse = [
{file = "dparse-0.6.2-py3-none-any.whl", hash = "sha256:8097076f1dd26c377f30d4745e6ec18fef42f3bf493933b842ac5bafad8c345f"},
{file = "dparse-0.6.2.tar.gz", hash = "sha256:d45255bda21f998bc7ddf2afd5e62505ba6134756ba2d42a84c56b0826614dfe"},
]
ecdsa = [
{file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"},
{file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"},
]
exceptiongroup = [
{file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"},
{file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"},
@ -2494,6 +2426,10 @@ Flask-Cors = [
{file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"},
{file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"},
]
flask-jwt-extended = [
{file = "Flask-JWT-Extended-4.4.4.tar.gz", hash = "sha256:62b521d75494c290a646ae8acc77123721e4364790f1e64af0038d823961fbf0"},
{file = "Flask_JWT_Extended-4.4.4-py2.py3-none-any.whl", hash = "sha256:a85eebfa17c339a7260c4643475af444784ba6de5588adda67406f0a75599553"},
]
Flask-Mail = [
{file = "Flask-Mail-0.9.1.tar.gz", hash = "sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41"},
]
@ -2988,10 +2924,6 @@ psycopg2 = [
{file = "psycopg2-2.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:849bd868ae3369932127f0771c08d1109b254f08d48dc42493c3d1b87cb2d308"},
{file = "psycopg2-2.9.4.tar.gz", hash = "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f"},
]
pyasn1 = [
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
]
pycodestyle = [
{file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
{file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
@ -3059,14 +2991,6 @@ python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
python-jose = [
{file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"},
{file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"},
]
python-keycloak = [
{file = "python-keycloak-2.6.0.tar.gz", hash = "sha256:08c530ff86f631faccb8033d9d9345cc3148cb2cf132ff7564f025292e4dbd96"},
{file = "python_keycloak-2.6.0-py3-none-any.whl", hash = "sha256:a1ce102b978beb56d385319b3ca20992b915c2c12d15a2d0c23f1104882f3fb6"},
]
pytz = [
{file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"},
{file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"},
@ -3205,10 +3129,6 @@ requests = [
{file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
]
requests-toolbelt = [
{file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
{file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
]
restrictedpython = [
{file = "RestrictedPython-6.0-py3-none-any.whl", hash = "sha256:3479303f7bff48a7dedad76f96e7704993c5e86c5adbd67f607295d5352f0fb8"},
{file = "RestrictedPython-6.0.tar.gz", hash = "sha256:405cf0bd9eec2f19b1326b5f48228efe56d6590b4e91826b8cc3b2cd400a96ad"},
@ -3216,10 +3136,6 @@ restrictedpython = [
restructuredtext-lint = [
{file = "restructuredtext_lint-1.4.0.tar.gz", hash = "sha256:1b235c0c922341ab6c530390892eb9e92f90b9b75046063e047cacfb0f050c45"},
]
rsa = [
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
{file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
]
"ruamel.yaml" = [
{file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"},
{file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"},

View File

@ -44,7 +44,6 @@ marshmallow-enum = "^1.5.1"
marshmallow-sqlalchemy = "^0.28.0"
PyJWT = "^2.6.0"
gunicorn = "^20.1.0"
python-keycloak = "^2.5.0"
APScheduler = "*"
Jinja2 = "^3.1.2"
RestrictedPython = "^6.0"
@ -72,6 +71,7 @@ simplejson = "^3.17.6"
pytz = "^2022.6"
dateparser = "^1.1.2"
types-dateparser = "^1.1.4.1"
flask-jwt-extended = "^4.4.4"
[tool.poetry.dev-dependencies]

View File

@ -23,6 +23,7 @@ from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_b
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import (
openid_blueprint,
)
from spiffworkflow_backend.routes.user import set_new_access_token_in_cookie
from spiffworkflow_backend.routes.user import verify_token
from spiffworkflow_backend.routes.user_blueprint import user_blueprint
from spiffworkflow_backend.services.authorization_service import AuthorizationService
@ -115,7 +116,7 @@ def create_app() -> flask.app.Flask:
r"^https?:\/\/%s(.*)" % o.replace(".", r"\.")
for o in app.config["CORS_ALLOW_ORIGINS"]
]
CORS(app, origins=origins_re, max_age=3600)
CORS(app, origins=origins_re, max_age=3600, supports_credentials=True)
connexion_app.add_api("api.yml", base_path=V1_API_PATH_PREFIX)
@ -131,6 +132,7 @@ def create_app() -> flask.app.Flask:
app.before_request(verify_token)
app.before_request(AuthorizationService.check_for_permission)
app.after_request(set_new_access_token_in_cookie)
return app # type: ignore

View File

@ -63,7 +63,6 @@ def setup_config(app: Flask) -> None:
)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config.from_object("spiffworkflow_backend.config.default")
print("loaded config: default")
env_config_prefix = "spiffworkflow_backend.config."
if (
@ -71,7 +70,6 @@ def setup_config(app: Flask) -> None:
and os.environ.get("SPIFFWORKFLOW_BACKEND_ENV") is not None
):
load_config_file(app, f"{env_config_prefix}terraform_deployed_environment")
print("loaded config: terraform_deployed_environment")
env_config_module = env_config_prefix + app.config["ENV_IDENTIFIER"]
load_config_file(app, env_config_module)
@ -90,14 +88,6 @@ def setup_config(app: Flask) -> None:
"permissions",
app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"],
)
print(
"set permissions file name config:"
f" {app.config['SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME']}"
)
print(
"set permissions file name full path:"
f" {app.config['PERMISSIONS_FILE_FULLPATH']}"
)
# unversioned (see .gitignore) config that can override everything and include secrets.
# src/spiffworkflow_backend/config/secrets.py

View File

@ -29,8 +29,11 @@ CONNECTOR_PROXY_URL = environ.get(
# Open ID server
OPEN_ID_SERVER_URL = environ.get(
"OPEN_ID_SERVER_URL", default="http://localhost:7002/realms/spiffworkflow"
"OPEN_ID_SERVER_URL",
default="http://localhost:7002/realms/spiffworkflow"
# "OPEN_ID_SERVER_URL", default="http://localhost:7000/openid"
)
# 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")

View File

@ -98,12 +98,14 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
"status": self.status,
"start_in_seconds": self.start_in_seconds,
"end_in_seconds": self.end_in_seconds,
"created_at_in_seconds": self.created_at_in_seconds,
"updated_at_in_seconds": self.updated_at_in_seconds,
"process_initiator_id": self.process_initiator_id,
"bpmn_xml_file_contents": self.bpmn_xml_file_contents,
"bpmn_version_control_identifier": self.bpmn_version_control_identifier,
"bpmn_version_control_type": self.bpmn_version_control_type,
"spiff_step": self.spiff_step,
"username": self.process_initiator.username,
"process_initiator_username": self.process_initiator.username,
}
@property

View File

@ -1,13 +1,11 @@
"""APIs for dealing with process groups, process models, and process instances."""
import json
import flask.wrappers
from flask import make_response
from flask.wrappers import Response
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
def status() -> flask.wrappers.Response:
def status() -> Response:
"""Status."""
ProcessInstanceModel.query.filter().first()
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
return make_response({"ok": True}, 200)

View File

@ -88,7 +88,7 @@ def process_group_list(
"pages": pages,
},
}
return Response(json.dumps(response_json), status=200, mimetype="application/json")
return make_response(jsonify(response_json), 200)
def process_group_show(

View File

@ -2,11 +2,13 @@
import ast
import base64
import json
import re
from typing import Any
from typing import Dict
from typing import Optional
from typing import Union
import flask
import jwt
from flask import current_app
from flask import g
@ -20,6 +22,7 @@ from spiffworkflow_backend.services.authentication_service import Authentication
from spiffworkflow_backend.services.authentication_service import (
MissingAccessTokenError,
)
from spiffworkflow_backend.services.authentication_service import TokenExpiredError
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.user_service import UserService
@ -55,6 +58,9 @@ def verify_token(
if not token and "Authorization" in request.headers:
token = request.headers["Authorization"].removeprefix("Bearer ")
# This should never be set here but just in case
_clear_auth_tokens_from_thread_local_data()
if token:
user_model = None
decoded_token = get_decoded_token(token)
@ -71,12 +77,11 @@ def verify_token(
f" internal token. {e}"
)
elif "iss" in decoded_token.keys():
user_info = None
try:
if AuthenticationService.validate_id_token(token):
if AuthenticationService.validate_id_or_access_token(token):
user_info = decoded_token
except (
ApiError
) as ae: # API Error is only thrown in the token is outdated.
except TokenExpiredError as token_expired_error:
# Try to refresh the token
user = UserService.get_user_by_service_and_service_id(
decoded_token["iss"], decoded_token["sub"]
@ -90,17 +95,24 @@ def verify_token(
)
)
if auth_token and "error" not in auth_token:
tld = current_app.config["THREAD_LOCAL_DATA"]
tld.new_access_token = auth_token["access_token"]
tld.new_id_token = auth_token["id_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}
else:
raise ae
else:
raise ae
else:
raise ae
user_info = {
"sub": user.service_id,
"iss": user.service,
}
if user_info is None:
raise ApiError(
error_code="invalid_token",
message="Your token is expired. Please Login",
status_code=401,
) from token_expired_error
except Exception as e:
current_app.logger.error(f"Exception raised in get_token: {e}")
raise ApiError(
error_code="fail_get_user_info",
message="Cannot get user info from token",
@ -150,8 +162,6 @@ def verify_token(
g.token = token
get_scope(token)
return None
# return {"uid": g.user.id, "sub": g.user.id, "scope": scope}
# return validate_scope(token, user_info, user_model)
else:
raise ApiError(error_code="no_user_id", message="Cannot get a user id")
@ -160,16 +170,44 @@ def verify_token(
)
def validate_scope(token: Any) -> bool:
"""Validate_scope."""
print("validate_scope")
# token = AuthenticationService.refresh_token(token)
# user_info = AuthenticationService.get_user_info_from_public_access_token(token)
# bearer_token = AuthenticationService.get_bearer_token(token)
# permission = AuthenticationService.get_permission_by_basic_token(token)
# permissions = AuthenticationService.get_permissions_by_token_for_resource_and_scope(token)
# introspection = AuthenticationService.introspect_token(basic_token)
return True
def set_new_access_token_in_cookie(
response: flask.wrappers.Response,
) -> flask.wrappers.Response:
"""Checks if a new token has been set in THREAD_LOCAL_DATA and sets cookies if appropriate.
It will also delete the cookies if the user has logged out.
"""
tld = current_app.config["THREAD_LOCAL_DATA"]
domain_for_frontend_cookie: Optional[str] = re.sub(
r"^https?:\/\/", "", current_app.config["SPIFFWORKFLOW_FRONTEND_URL"]
)
if domain_for_frontend_cookie and domain_for_frontend_cookie.startswith(
"localhost"
):
domain_for_frontend_cookie = None
if hasattr(tld, "new_access_token") and tld.new_access_token:
response.set_cookie(
"access_token", tld.new_access_token, domain=domain_for_frontend_cookie
)
# id_token is required for logging out since this gets passed back to the openid server
if hasattr(tld, "new_id_token") and tld.new_id_token:
response.set_cookie(
"id_token", tld.new_id_token, domain=domain_for_frontend_cookie
)
if hasattr(tld, "user_has_logged_out") and tld.user_has_logged_out:
response.set_cookie(
"id_token", "", max_age=0, domain=domain_for_frontend_cookie
)
response.set_cookie(
"access_token", "", max_age=0, domain=domain_for_frontend_cookie
)
_clear_auth_tokens_from_thread_local_data()
return response
def encode_auth_token(sub: str, token_type: Optional[str] = None) -> str:
@ -226,7 +264,7 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response
user_info = parse_id_token(id_token)
if AuthenticationService.validate_id_token(id_token):
if AuthenticationService.validate_id_or_access_token(id_token):
if user_info and "error" not in user_info:
user_model = AuthorizationService.create_user_from_sign_in(user_info)
g.user = user_model.id
@ -234,11 +272,10 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response
AuthenticationService.store_refresh_token(
user_model.id, auth_token_object["refresh_token"]
)
redirect_url = (
f"{state_redirect_url}?"
+ f"access_token={auth_token_object['access_token']}&"
+ f"id_token={id_token}"
)
redirect_url = state_redirect_url
tld = current_app.config["THREAD_LOCAL_DATA"]
tld.new_access_token = auth_token_object["access_token"]
tld.new_id_token = auth_token_object["id_token"]
return redirect(redirect_url)
raise ApiError(
@ -284,6 +321,8 @@ def logout(id_token: str, redirect_url: Optional[str]) -> Response:
"""Logout."""
if redirect_url is None:
redirect_url = ""
tld = current_app.config["THREAD_LOCAL_DATA"]
tld.user_has_logged_out = True
return AuthenticationService().logout(redirect_url=redirect_url, id_token=id_token)
@ -312,15 +351,6 @@ def get_decoded_token(token: str) -> Optional[Dict]:
error_code="unknown_token",
message="Unknown token type in get_decoded_token",
)
# try:
# # see if we have an open_id token
# decoded_token = AuthorizationService.decode_auth_token(token)
# else:
# if 'sub' in decoded_token and 'iss' in decoded_token and 'aud' in decoded_token:
# token_type = 'id_token'
# if 'token_type' in decoded_token and 'sub' in decoded_token:
# return True
def get_scope(token: str) -> str:
@ -347,3 +377,14 @@ def get_user_from_decoded_internal_token(decoded_token: dict) -> Optional[UserMo
return user
user = UserService.create_user(service_id, service, service_id)
return user
def _clear_auth_tokens_from_thread_local_data() -> None:
"""_clear_auth_tokens_from_thread_local_data."""
tld = current_app.config["THREAD_LOCAL_DATA"]
if hasattr(tld, "new_access_token"):
delattr(tld, "new_access_token")
if hasattr(tld, "new_id_token"):
delattr(tld, "new_id_token")
if hasattr(tld, "user_has_logged_out"):
delattr(tld, "user_has_logged_out")

View File

@ -20,6 +20,15 @@ class MissingAccessTokenError(Exception):
"""MissingAccessTokenError."""
# These could be either 'id' OR 'access' tokens and we can't always know which
class TokenExpiredError(Exception):
"""TokenExpiredError."""
class TokenInvalidError(Exception):
"""TokenInvalidError."""
class AuthenticationProviderTypes(enum.Enum):
"""AuthenticationServiceProviders."""
@ -125,18 +134,15 @@ class AuthenticationService:
return auth_token_object
@classmethod
def validate_id_token(cls, id_token: str) -> bool:
def validate_id_or_access_token(cls, token: str) -> bool:
"""Https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation."""
valid = True
now = time.time()
try:
decoded_token = jwt.decode(id_token, options={"verify_signature": False})
decoded_token = jwt.decode(token, options={"verify_signature": False})
except Exception as e:
raise ApiError(
error_code="bad_id_token",
message="Cannot decode id_token",
status_code=401,
) from e
raise TokenInvalidError("Cannot decode token") from e
if decoded_token["iss"] != cls.server_url():
valid = False
elif (
@ -153,15 +159,10 @@ class AuthenticationService:
valid = False
if not valid:
current_app.logger.error(f"Invalid token in validate_id_token: {id_token}")
return False
if now > decoded_token["exp"]:
raise ApiError(
error_code="invalid_token",
message="Your token is expired. Please Login",
status_code=401,
)
raise TokenExpiredError("Your token is expired. Please Login")
return True

View File

@ -412,59 +412,6 @@ class AuthorizationService:
status_code=403,
)
# def refresh_token(self, token: str) -> str:
# """Refresh_token."""
# # if isinstance(token, str):
# # token = eval(token)
# (
# open_id_server_url,
# open_id_client_id,
# open_id_realm_name,
# open_id_client_secret_key,
# ) = AuthorizationService.get_open_id_args()
# headers = {"Content-Type": "application/x-www-form-urlencoded"}
# request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token"
# data = {
# "grant_type": "refresh_token",
# "client_id": "spiffworkflow-frontend",
# "subject_token": token,
# "refresh_token": token,
# }
# refresh_response = requests.post(request_url, headers=headers, data=data)
# refresh_token = json.loads(refresh_response.text)
# return refresh_token
# def get_bearer_token(self, basic_token: str) -> dict:
# """Get_bearer_token."""
# (
# open_id_server_url,
# open_id_client_id,
# open_id_realm_name,
# open_id_client_secret_key,
# ) = AuthorizationService.get_open_id_args()
#
# backend_basic_auth_string = f"{open_id_client_id}:{open_id_client_secret_key}"
# backend_basic_auth_bytes = bytes(backend_basic_auth_string, encoding="ascii")
# backend_basic_auth = base64.b64encode(backend_basic_auth_bytes)
#
# headers = {
# "Content-Type": "application/x-www-form-urlencoded",
# "Authorization": f"Basic {backend_basic_auth.decode('utf-8')}",
# }
# data = {
# "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
# "client_id": open_id_client_id,
# "subject_token": basic_token,
# "audience": open_id_client_id,
# }
# request_url = f"{open_id_server_url}/realms/{open_id_realm_name}/protocol/openid-connect/token"
#
# backend_response = requests.post(request_url, headers=headers, data=data)
# # json_data = json.loads(backend_response.text)
# # bearer_token = json_data['access_token']
# bearer_token: dict = json.loads(backend_response.text)
# return bearer_token
@staticmethod
def decode_auth_token(auth_token: str) -> dict[str, Union[str, None]]:
"""Decode the auth token.

View File

@ -8,6 +8,7 @@ from flask.wrappers import Response
from typing import List
from typing import Union
from flask import current_app
from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db
@ -71,7 +72,7 @@ class ErrorHandlingService:
self.handle_system_notification(_error, process_model)
except Exception as e:
# hmm... what to do if a notification method fails. Probably log, at least
print(e)
current_app.logger.error(e)
@staticmethod
def handle_system_notification(

View File

@ -42,7 +42,6 @@ class MessageService:
message_type="receive", status="ready"
).all()
for message_instance_send in message_instances_send:
# print(f"message_instance_send.id: {message_instance_send.id}")
# check again in case another background process picked up the message
# while the previous one was running
if message_instance_send.status != "ready":

View File

@ -338,7 +338,11 @@ class ProcessInstanceReportService:
},
{"Header": "Start", "accessor": "start_in_seconds", "filterable": False},
{"Header": "End", "accessor": "end_in_seconds", "filterable": False},
{"Header": "Username", "accessor": "username", "filterable": False},
{
"Header": "Started By",
"accessor": "process_initiator_username",
"filterable": False,
},
{"Header": "Status", "accessor": "status", "filterable": False},
]

View File

@ -55,9 +55,6 @@ class ServiceTaskDelegate:
f"{connector_proxy_url()}/v1/do/{name}", json=params
)
if proxied_response.status_code != 200:
print("got error from connector proxy")
parsed_response = json.loads(proxied_response.text)
if "refreshed_token_set" not in parsed_response:
@ -86,7 +83,7 @@ class ServiceTaskService:
parsed_response = json.loads(response.text)
return parsed_response
except Exception as e:
print(e)
current_app.logger.error(e)
return []
@staticmethod

View File

@ -232,7 +232,7 @@ class TestProcessApi(BaseTest):
"process_model_display_name",
"start_in_seconds",
"end_in_seconds",
"username",
"process_initiator_username",
"status",
"summary",
"description",
@ -3096,7 +3096,7 @@ class TestProcessApi(BaseTest):
assert response.json["pagination"]["pages"] == 1
assert response.json["pagination"]["total"] == 1
def test_can_get_process_instance_list_with_report_metadata_and_process_initator(
def test_can_get_process_instance_list_with_report_metadata_and_process_initiator(
self,
app: Flask,
client: FlaskClient,
@ -3174,8 +3174,14 @@ class TestProcessApi(BaseTest):
assert response.json is not None
assert response.status_code == 200
assert len(response.json["results"]) == 2
assert response.json["results"][0]["username"] == user_one.username
assert response.json["results"][1]["username"] == user_one.username
assert (
response.json["results"][0]["process_initiator_username"]
== user_one.username
)
assert (
response.json["results"][1]["process_initiator_username"]
== user_one.username
)
response = client.get(
f"/v1.0/process-instances?report_identifier={process_instance_report_dne.identifier}",
@ -3227,7 +3233,11 @@ class TestProcessApi(BaseTest):
},
{"Header": "Start", "accessor": "start_in_seconds", "filterable": False},
{"Header": "End", "accessor": "end_in_seconds", "filterable": False},
{"Header": "Username", "accessor": "username", "filterable": False},
{
"Header": "Started By",
"accessor": "process_initiator_username",
"filterable": False,
},
{"Header": "Status", "accessor": "status", "filterable": False},
{"Header": "key1", "accessor": "key1", "filterable": True},
{"Header": "key2", "accessor": "key2", "filterable": True},

View File

@ -39,6 +39,7 @@
"bpmn-js": "^9.3.2",
"bpmn-js-properties-panel": "^1.10.0",
"bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main",
"cookie": "^0.5.0",
"craco": "^0.0.3",
"date-fns": "^2.28.0",
"diagram-js": "^8.5.0",
@ -66,6 +67,7 @@
},
"devDependencies": {
"@cypress/grep": "^3.1.0",
"@types/cookie": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6",
"cypress": "^12",
@ -5654,6 +5656,12 @@
"@types/node": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
"dev": true
},
"node_modules/@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@ -35330,6 +35338,12 @@
"@types/node": "*"
}
},
"@types/cookie": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
"dev": true
},
"@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",

View File

@ -34,6 +34,7 @@
"bpmn-js": "^9.3.2",
"bpmn-js-properties-panel": "^1.10.0",
"bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main",
"cookie": "^0.5.0",
"craco": "^0.0.3",
"date-fns": "^2.28.0",
"diagram-js": "^8.5.0",
@ -102,6 +103,7 @@
},
"devDependencies": {
"@cypress/grep": "^3.1.0",
"@types/cookie": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6",
"cypress": "^12",

View File

@ -1171,7 +1171,7 @@ export default function ProcessInstanceListTable({
start_in_seconds: 'Start Time',
end_in_seconds: 'End Time',
status: 'Status',
username: 'Started By',
process_initiator_username: 'Started By',
spiff_step: 'SpiffWorkflow Step',
};
const getHeaderLabel = (header: string) => {

View File

@ -12,7 +12,6 @@ if (/^\d+\./.test(hostname) || hostname === 'localhost') {
}
let url = `${protocol}://${hostAndPort}/v1.0`;
// Allow overriding the backend base url with an environment variable at build time.
if (process.env.REACT_APP_BACKEND_BASE_URL) {
url = process.env.REACT_APP_BACKEND_BASE_URL;
}

View File

@ -20,7 +20,7 @@ const doRender = () => {
);
};
UserService.getAuthTokenFromParams();
UserService.loginIfNeeded();
doRender();
// If you want to start measuring performance in your app, pass a function

View File

@ -73,8 +73,13 @@ export interface ProcessInstance {
status: string;
start_in_seconds: number | null;
end_in_seconds: number | null;
process_initiator_username: string;
bpmn_xml_file_contents?: string;
spiff_step?: number;
created_at_in_seconds: number;
updated_at_in_seconds: number;
bpmn_version_control_identifier: string;
bpmn_version_control_type: string;
}
export interface MessageCorrelationProperties {

View File

@ -42,7 +42,7 @@ export default function AuthenticationList() {
row.id
}?redirect_url=${redirectUrl}/${
row.id
}?token=${UserService.getAuthToken()}`}
}?token=${UserService.getAccessToken()}`}
>
{row.id}
</a>

View File

@ -73,11 +73,6 @@ export default function JsonSchemaFormBuilder() {
};
const onFormFieldTitleChange = (newFormFieldTitle: string) => {
console.log('newFormFieldTitle', newFormFieldTitle);
console.log(
'setFormFieldIdHasBeenUpdatedByUser',
formFieldIdHasBeenUpdatedByUser
);
if (!formFieldIdHasBeenUpdatedByUser) {
setFormFieldId(underscorizeString(newFormFieldTitle));
}

View File

@ -7,12 +7,12 @@ import {
useSearchParams,
} from 'react-router-dom';
import {
CaretRight,
TrashCan,
StopOutline,
PauseOutline,
PlayOutline,
CaretLeft,
CaretRight,
InProgress,
Checkmark,
Warning,
@ -72,6 +72,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const [eventPayload, setEventPayload] = useState<string>('{}');
const [eventTextEditorEnabled, setEventTextEditorEnabled] =
useState<boolean>(false);
const [displayDetails, setDisplayDetails] = useState<boolean>(false);
const setErrorObject = (useContext as any)(ErrorContext)[1];
@ -280,6 +281,70 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
});
};
const detailedViewElement = () => {
if (!processInstance) {
return null;
}
if (displayDetails) {
return (
<>
<Grid condensed fullWidth>
<Button
kind="ghost"
className="button-link"
onClick={() => setDisplayDetails(false)}
title="Hide Details"
>
&laquo; Hide Details
</Button>
</Grid>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={2} className="grid-list-title">
Updated At:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
{convertSecondsToFormattedDateTime(
processInstance.updated_at_in_seconds
)}
</Column>
</Grid>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={2} className="grid-list-title">
Created At:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
{convertSecondsToFormattedDateTime(
processInstance.created_at_in_seconds
)}
</Column>
</Grid>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={2} className="grid-list-title">
Process model revision:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
{processInstance.bpmn_version_control_identifier} (
{processInstance.bpmn_version_control_type})
</Column>
</Grid>
</>
);
}
return (
<Grid condensed fullWidth>
<Button
kind="ghost"
className="button-link"
onClick={() => setDisplayDetails(true)}
title="Show Details"
>
View Details &raquo;
</Button>
</Grid>
);
};
const getInfoTag = () => {
if (!processInstance) {
return null;
@ -316,6 +381,14 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return (
<>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={2} className="grid-list-title">
Started By:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
{processInstance.process_initiator_username}
</Column>
</Grid>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={2} className="grid-list-title">
Started:{' '}
@ -337,6 +410,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
</Tag>
</Column>
</Grid>
{detailedViewElement()}
<br />
<Grid condensed fullWidth>
<Column sm={2} md={2} lg={2}>

View File

@ -184,6 +184,13 @@ export default function TaskShow() {
);
}
function customValidate(formData: any, errors: any) {
if (formData.pass1 !== formData.pass2) {
errors.pass2.addError("Passwords don't match");
}
return errors;
}
return (
<Grid fullWidth condensed>
<Column md={5} lg={8} sm={4}>
@ -193,6 +200,7 @@ export default function TaskShow() {
schema={jsonSchema}
uiSchema={formUiSchema}
validator={validator}
customValidate={customValidate}
>
{reactFragmentToHideSubmitButton}
</Form>

View File

@ -11,7 +11,7 @@ const HttpMethods = {
const getBasicHeaders = (): object => {
if (UserService.isLoggedIn()) {
return {
Authorization: `Bearer ${UserService.getAuthToken()}`,
Authorization: `Bearer ${UserService.getAccessToken()}`,
};
}
return {};
@ -64,6 +64,7 @@ backendCallProps) => {
Object.assign(httpArgs, {
headers: new Headers(headers as any),
method: httpMethod,
credentials: 'include',
});
const updatedPath = path.replace(/^\/v1\.0/, '');

View File

@ -1,4 +1,5 @@
import jwt from 'jwt-decode';
import cookie from 'cookie';
import { BACKEND_BASE_URL } from '../config';
// NOTE: this currently stores the jwt token in local storage
@ -10,37 +11,46 @@ import { BACKEND_BASE_URL } from '../config';
// Some explanation:
// https://dev.to/nilanth/how-to-secure-jwt-in-a-single-page-application-cko
// const getCurrentLocation = (queryParams: string = window.location.search) => {
const getCurrentLocation = () => {
const queryParamString = '';
// if (queryParams) {
// queryParamString = `?${queryParams}`;
// }
return `${window.location.origin}${window.location.pathname}${queryParamString}`;
const getCookie = (key: string) => {
const parsedCookies = cookie.parse(document.cookie);
if (key in parsedCookies) {
return parsedCookies[key];
}
return null;
};
const getCurrentLocation = (queryParams: string = window.location.search) => {
let queryParamString = '';
if (queryParams) {
queryParamString = `${queryParams}`;
}
return encodeURIComponent(
`${window.location.origin}${window.location.pathname}${queryParamString}`
);
};
const doLogin = () => {
const url = `${BACKEND_BASE_URL}/login?redirect_url=${getCurrentLocation()}`;
window.location.href = url;
};
// required for logging out
const getIdToken = () => {
return localStorage.getItem('jwtIdToken');
return getCookie('id_token');
};
const doLogout = () => {
const idToken = getIdToken();
localStorage.removeItem('jwtAccessToken');
localStorage.removeItem('jwtIdToken');
const redirectUrl = `${window.location.origin}`;
const url = `${BACKEND_BASE_URL}/logout?redirect_url=${redirectUrl}&id_token=${idToken}`;
window.location.href = url;
};
const getAuthToken = () => {
return localStorage.getItem('jwtAccessToken');
const getAccessToken = () => {
return getCookie('access_token');
};
const isLoggedIn = () => {
return !!getAuthToken();
return !!getAccessToken();
};
const getUserEmail = () => {
@ -61,24 +71,8 @@ const getPreferredUsername = () => {
return null;
};
// FIXME: we could probably change this search to a hook
// and then could use useSearchParams here instead
const getAuthTokenFromParams = () => {
const queryParams = new URLSearchParams(window.location.search);
const accessToken = queryParams.get('access_token');
const idToken = queryParams.get('id_token');
queryParams.delete('access_token');
queryParams.delete('id_token');
if (accessToken) {
localStorage.setItem('jwtAccessToken', accessToken);
if (idToken) {
localStorage.setItem('jwtIdToken', idToken);
}
// window.location.href = `${getCurrentLocation(queryParams.toString())}`;
window.location.href = `${getCurrentLocation()}`;
} else if (!isLoggedIn()) {
const loginIfNeeded = () => {
if (!isLoggedIn()) {
doLogin();
}
};
@ -91,8 +85,8 @@ const UserService = {
doLogin,
doLogout,
isLoggedIn,
getAuthToken,
getAuthTokenFromParams,
getAccessToken,
loginIfNeeded,
getPreferredUsername,
getUserEmail,
hasRole,

View File

@ -45,7 +45,6 @@ export default function BaseInputTemplate<
// Note: since React 15.2.0 we can't forward unknown element attributes, so we
// exclude the "options" and "schema" ones here.
if (!id) {
console.log('No id for', props);
throw new Error(`no id for props ${JSON.stringify(props)}`);
}
const inputProps = {