Merge pull request #84 from sartography/feature/bpmn_user_permissions

Feature/bpmn user permissions
This commit is contained in:
jasquat 2022-12-22 16:56:45 -05:00 committed by GitHub
commit bcc939d6f3
46 changed files with 1486 additions and 600 deletions

View File

@ -7,7 +7,8 @@ def main() -> None:
"""Main.""" """Main."""
app = get_hacked_up_app_for_script() app = get_hacked_up_app_for_script()
with app.app_context(): with app.app_context():
AuthorizationService.delete_all_permissions_and_recreate() AuthorizationService.delete_all_permissions()
AuthorizationService.import_permissions_from_yaml_file()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -424,6 +424,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "admin@status.im",
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"credentials" : [ { "credentials" : [ {
@ -446,6 +447,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "alex@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "81a61a3b-228d-42b3-b39a-f62d8e7f57ca", "id" : "81a61a3b-228d-42b3-b39a-f62d8e7f57ca",
"type" : "password", "type" : "password",
@ -465,6 +467,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "amir@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "e589f3ad-bf7b-4756-89f7-7894c03c2831", "id" : "e589f3ad-bf7b-4756-89f7-7894c03c2831",
"type" : "password", "type" : "password",
@ -484,6 +487,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "ciadmin1@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "111b5ea1-c2ab-470a-a16b-2373bc94de7a", "id" : "111b5ea1-c2ab-470a-a16b-2373bc94de7a",
"type" : "password", "type" : "password",
@ -506,6 +510,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "ciuser1@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "762f36e9-47af-44da-8520-cf09d752497a", "id" : "762f36e9-47af-44da-8520-cf09d752497a",
"type" : "password", "type" : "password",
@ -528,6 +533,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "core@status.im",
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"credentials" : [ { "credentials" : [ {
@ -550,6 +556,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "dan@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "d517c520-f500-4542-80e5-7144daef1e32", "id" : "d517c520-f500-4542-80e5-7144daef1e32",
"type" : "password", "type" : "password",
@ -569,6 +576,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "daniel@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "f240495c-265b-42fc-99db-46928580d07d", "id" : "f240495c-265b-42fc-99db-46928580d07d",
"type" : "password", "type" : "password",
@ -588,6 +596,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "elizabeth@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "ae951ec8-9fc9-4f1b-b340-bbbe463ae5c2", "id" : "ae951ec8-9fc9-4f1b-b340-bbbe463ae5c2",
"type" : "password", "type" : "password",
@ -607,6 +616,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "fin@status.im",
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"credentials" : [ { "credentials" : [ {
@ -629,6 +639,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "fin1@status.im",
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"credentials" : [ { "credentials" : [ {
@ -651,6 +662,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "finance_user1@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "f14722ec-13a7-4d35-a4ec-0475d405ae58", "id" : "f14722ec-13a7-4d35-a4ec-0475d405ae58",
"type" : "password", "type" : "password",
@ -670,6 +682,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "harmeet@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "89c26090-9bd3-46ac-b038-883d02e3f125", "id" : "89c26090-9bd3-46ac-b038-883d02e3f125",
"type" : "password", "type" : "password",
@ -689,6 +702,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "j@status.im",
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"credentials" : [ { "credentials" : [ {
@ -711,6 +725,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "jakub@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "ce141fa5-b8d5-4bbe-93e7-22e7119f97c2", "id" : "ce141fa5-b8d5-4bbe-93e7-22e7119f97c2",
"type" : "password", "type" : "password",
@ -730,6 +745,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "jarrad@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "113e0343-1069-476d-83f9-21d98edb9cfa", "id" : "113e0343-1069-476d-83f9-21d98edb9cfa",
"type" : "password", "type" : "password",
@ -749,6 +765,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "jason@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "40abf32e-f0cc-4a17-8231-1a69a02c1b0b", "id" : "40abf32e-f0cc-4a17-8231-1a69a02c1b0b",
"type" : "password", "type" : "password",
@ -768,6 +785,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "jon@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "8b520e01-5b9b-44ab-9ee8-505bd0831a45", "id" : "8b520e01-5b9b-44ab-9ee8-505bd0831a45",
"type" : "password", "type" : "password",
@ -787,6 +805,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "kb@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "2c0be363-038f-48f1-86d6-91fdd28657cf", "id" : "2c0be363-038f-48f1-86d6-91fdd28657cf",
"type" : "password", "type" : "password",
@ -806,6 +825,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "lead@status.im",
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"credentials" : [ { "credentials" : [ {
@ -828,6 +848,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "lead1@status.im",
"firstName" : "", "firstName" : "",
"lastName" : "", "lastName" : "",
"credentials" : [ { "credentials" : [ {
@ -850,6 +871,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "manuchehr@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "07dabf55-b5d3-4f98-abba-3334086ecf5e", "id" : "07dabf55-b5d3-4f98-abba-3334086ecf5e",
"type" : "password", "type" : "password",
@ -869,6 +891,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "mike@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "1ed375fb-0f1a-4c2a-9243-2477242cf7bd", "id" : "1ed375fb-0f1a-4c2a-9243-2477242cf7bd",
"type" : "password", "type" : "password",
@ -888,6 +911,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "natalia@sartography.com",
"credentials" : [ { "credentials" : [ {
"id" : "b6aa9936-39cc-4931-bfeb-60e6753de5ba", "id" : "b6aa9936-39cc-4931-bfeb-60e6753de5ba",
"type" : "password", "type" : "password",
@ -907,6 +931,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "sasha@status.im",
"credentials" : [ { "credentials" : [ {
"id" : "4a170af4-6f0c-4e7b-b70c-e674edf619df", "id" : "4a170af4-6f0c-4e7b-b70c-e674edf619df",
"type" : "password", "type" : "password",
@ -926,6 +951,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "service-account@status.im",
"serviceAccountClientId" : "spiffworkflow-backend", "serviceAccountClientId" : "spiffworkflow-backend",
"credentials" : [ ], "credentials" : [ ],
"disableableCredentialTypes" : [ ], "disableableCredentialTypes" : [ ],
@ -943,6 +969,7 @@
"enabled" : true, "enabled" : true,
"totp" : false, "totp" : false,
"emailVerified" : false, "emailVerified" : false,
"email": "service-account-withauth@status.im",
"serviceAccountClientId" : "withAuth", "serviceAccountClientId" : "withAuth",
"credentials" : [ ], "credentials" : [ ],
"disableableCredentialTypes" : [ ], "disableableCredentialTypes" : [ ],

View File

@ -1,8 +1,8 @@
"""empty message """empty message
Revision ID: b99a4cb94b5b Revision ID: 67197b02b0c1
Revises: Revises:
Create Date: 2022-12-20 10:45:08.295317 Create Date: 2022-12-20 15:05:31.545567
""" """
from alembic import op from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'b99a4cb94b5b' revision = '67197b02b0c1'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -72,16 +72,15 @@ def upgrade():
op.create_table('user', op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=255), nullable=False), sa.Column('username', sa.String(length=255), nullable=False),
sa.Column('uid', sa.String(length=50), nullable=True),
sa.Column('service', sa.String(length=50), nullable=False), sa.Column('service', sa.String(length=50), nullable=False),
sa.Column('service_id', sa.String(length=255), nullable=False), sa.Column('service_id', sa.String(length=255), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True), sa.Column('display_name', sa.String(length=255), nullable=True),
sa.Column('email', sa.String(length=255), nullable=True), sa.Column('email', sa.String(length=255), nullable=True),
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True),
sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), sa.Column('created_at_in_seconds', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('service', 'service_id', name='service_key'), sa.UniqueConstraint('service', 'service_id', name='service_key'),
sa.UniqueConstraint('uid') sa.UniqueConstraint('username')
) )
op.create_table('message_correlation_property', op.create_table('message_correlation_property',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
@ -176,6 +175,14 @@ def upgrade():
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'group_id', name='user_group_assignment_unique') sa.UniqueConstraint('user_id', 'group_id', name='user_group_assignment_unique')
) )
op.create_table('user_group_assignment_waiting',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=255), nullable=False),
sa.Column('group_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['group_id'], ['group.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username', 'group_id', name='user_group_assignment_staged_unique')
)
op.create_table('human_task', op.create_table('human_task',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('process_instance_id', sa.Integer(), nullable=False), sa.Column('process_instance_id', sa.Integer(), nullable=False),
@ -309,6 +316,7 @@ def downgrade():
op.drop_table('message_correlation') op.drop_table('message_correlation')
op.drop_index(op.f('ix_human_task_completed'), table_name='human_task') op.drop_index(op.f('ix_human_task_completed'), table_name='human_task')
op.drop_table('human_task') op.drop_table('human_task')
op.drop_table('user_group_assignment_waiting')
op.drop_table('user_group_assignment') op.drop_table('user_group_assignment')
op.drop_table('secret') op.drop_table('secret')
op.drop_table('refresh_token') op.drop_table('refresh_token')

View File

@ -18,6 +18,7 @@ from werkzeug.exceptions import NotFound
import spiffworkflow_backend.load_database_models # noqa: F401 import spiffworkflow_backend.load_database_models # noqa: F401
from spiffworkflow_backend.config import setup_config from spiffworkflow_backend.config import setup_config
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import ( from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import (
openid_blueprint, openid_blueprint,
@ -117,7 +118,7 @@ def create_app() -> flask.app.Flask:
] ]
CORS(app, origins=origins_re, max_age=3600) CORS(app, origins=origins_re, max_age=3600)
connexion_app.add_api("api.yml", base_path="/v1.0") connexion_app.add_api("api.yml", base_path=V1_API_PATH_PREFIX)
mail = Mail(app) mail = Mail(app)
app.config["MAIL_APP"] = mail app.config["MAIL_APP"] = mail

View File

@ -10,55 +10,52 @@ groups:
admin: admin:
users: users:
[ [
admin, admin@status.im,
jakub, jakub@status.im,
kb, jarrad@status.im,
alex, kb@sartography.com,
dan, alex@sartography.com,
mike, dan@sartography.com,
jason, mike@sartography.com,
jarrad, jason@sartography.com,
elizabeth, j@sartography.com,
jon, elizabeth@sartography.com,
jon@sartography.com,
] ]
Finance Team: Finance Team:
users: users:
[ [
jakub, jakub@status.im,
alex, amir@status.im,
dan, jarrad@status.im,
mike, sasha@status.im,
jason, fin@sartography.com,
amir, fin1@sartography.com,
jarrad, alex@sartography.com,
elizabeth, dan@sartography.com,
jon, mike@sartography.com,
sasha, jason@sartography.com,
fin, j@sartography.com,
fin1, elizabeth@sartography.com,
jon@sartography.com,
] ]
demo: demo:
users: users:
[ [
core, harmeet@status.im,
fin, sasha@status.im,
fin1, manuchehr@status.im,
harmeet, core@status.im,
jason, fin@status.im,
sasha, fin1@status.im,
manuchehr, lead@status.im,
lead, lead1@status.im
lead1
] ]
core-contributor: test:
users: users: [natalia@sartography.com]
[
core,
harmeet,
]
admin-ro: admin-ro:
users: users:
@ -66,16 +63,12 @@ groups:
j, j,
] ]
test:
users: [natalia]
permissions: permissions:
admin: admin:
groups: [admin] groups: [admin]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*
admin-readonly: admin-readonly:
groups: [admin-ro] groups: [admin-ro]
users: [] users: []
@ -85,121 +78,93 @@ permissions:
groups: [admin-ro] groups: [admin-ro]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/* uri: /process-instances/*
tasks-crud: # open system defaults for everybody
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/service-tasks
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/user-groups/for-current-user
# read all for everybody
read-all-process-groups: read-all-process-groups:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-groups/* uri: /process-groups/*
read-all-process-models: read-all-process-models:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-models/* uri: /process-models/*
# basic perms for everybody
read-all-process-instances-for-me: read-all-process-instances-for-me:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-instances/for-me/* uri: /process-instances/for-me/*
read-process-instance-reports: read-process-instance-reports:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/reports/* uri: /process-instances/reports/*
processes-read: processes-read:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/processes uri: /processes
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /service-tasks
tasks-crud:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /tasks/*
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /user-groups/for-current-user
manage-procurement-admin:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:*
manage-procurement-admin-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement/*
manage-procurement-admin-models:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement:*
manage-procurement-admin-models-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement/*
manage-procurement-admin-instances:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement:*
manage-procurement-admin-instances-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement/*
finance-admin: finance-admin:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:procurement:* uri: /process-groups/manage-procurement:procurement:*
manage-revenue-streams-instances: manage-revenue-streams-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/* uri: /process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances: manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:* uri: /process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances: manage-procurement-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:* uri: /process-instances/manage-procurement:vendor-lifecycle-management:*
manage-revenue-streams-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:vendor-lifecycle-management:*
create-test-instances: create-test-instances:
groups: ["test"] groups: ["test"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:test:* uri: /process-instances/misc:test:*
core1-admin-instances:
groups: ["core-contributor", "Finance Team"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form:*
core1-admin-instances-slash:
groups: ["core-contributor", "Finance Team"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form/*

View File

@ -2,14 +2,17 @@ default_group: everybody
users: users:
admin: admin:
service: local_open_id
email: admin@spiffworkflow.org email: admin@spiffworkflow.org
password: admin password: admin
preferred_username: Admin preferred_username: Admin
nelson: nelson:
service: local_open_id
email: nelson@spiffworkflow.org email: nelson@spiffworkflow.org
password: nelson password: nelson
preferred_username: Nelson preferred_username: Nelson
malala: malala:
service: local_open_id
email: malala@spiffworkflow.org email: malala@spiffworkflow.org
password: malala password: malala
preferred_username: Malala preferred_username: Malala
@ -18,17 +21,17 @@ groups:
admin: admin:
users: users:
[ [
admin, admin@spiffworkflow.org,
] ]
Education: Education:
users: users:
[ [
malala malala@spiffworkflow.org
] ]
President: President:
users: users:
[ [
nelson nelson@spiffworkflow.org
] ]
permissions: permissions:
@ -44,45 +47,44 @@ permissions:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/* uri: /tasks/*
# Everyone can see everything (all groups, and processes are visible) # Everyone can see everything (all groups, and processes are visible)
read-all-process-groups: read-all-process-groups:
groups: [ everybody ] groups: [ everybody ]
users: [ ] users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /v1.0/process-groups/* uri: /process-groups/*
read-all-process-models: read-all-process-models:
groups: [ everybody ] groups: [ everybody ]
users: [ ] users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /v1.0/process-models/* uri: /process-models/*
read-all-process-instance: read-all-process-instance:
groups: [ everybody ] groups: [ everybody ]
users: [ ] users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /v1.0/process-instances/* uri: /process-instances/*
read-process-instance-reports: read-process-instance-reports:
groups: [ everybody ] groups: [ everybody ]
users: [ ] users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /v1.0/process-instances/reports/* uri: /process-instances/reports/*
processes-read: processes-read:
groups: [ everybody ] groups: [ everybody ]
users: [ ] users: [ ]
allowed_permissions: [ read ] allowed_permissions: [ read ]
uri: /v1.0/processes uri: /processes
# Members of the Education group can change the processes under "education".
# Members of the Education group can change they processes work.
education-admin: education-admin:
groups: ["Education", "President"] groups: ["Education", "President"]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/education:* uri: /process-groups/education:*
# Anyone can start an education process. # Anyone can start an education process.
education-everybody: education-everybody:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:category_number_one:process-model-with-form/* uri: /process-instances/misc:category_number_one:process-model-with-form/*

View File

@ -4,57 +4,48 @@ groups:
admin: admin:
users: users:
[ [
admin, admin@status.im,
jakub, jakub@status.im,
kb, jarrad@status.im,
alex, kb@sartography.com,
dan, alex@sartography.com,
mike, dan@sartography.com,
jason, mike@sartography.com,
j, jason@sartography.com,
jarrad, j@sartography.com,
elizabeth, elizabeth@sartography.com,
jon, jon@sartography.com,
natalia,
] ]
Finance Team: Finance Team:
users: users:
[ [
jakub, jakub@status.im,
alex, amir@status.im,
dan, jarrad@status.im,
mike, sasha@status.im,
jason, fin@sartography.com,
j, fin1@sartography.com,
amir, alex@sartography.com,
jarrad, dan@sartography.com,
elizabeth, mike@sartography.com,
jon, jason@sartography.com,
natalia, j@sartography.com,
sasha, elizabeth@sartography.com,
fin, jon@sartography.com,
fin1,
] ]
demo: demo:
users: users:
[ [
core, harmeet@status.im,
fin, sasha@status.im,
fin1, manuchehr@status.im,
harmeet, core@status.im,
sasha, fin@status.im,
manuchehr, fin1@status.im,
lead, lead@status.im,
lead1 lead1@status.im
]
core-contributor:
users:
[
core,
harmeet,
] ]
permissions: permissions:
@ -67,104 +58,86 @@ permissions:
groups: [admin] groups: [admin]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/* uri: /process-instances/*
tasks-crud: # open system defaults for everybody
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/service-tasks
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/user-groups/for-current-user
# read all for everybody
read-all-process-groups: read-all-process-groups:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-groups/* uri: /process-groups/*
read-all-process-models: read-all-process-models:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-models/* uri: /process-models/*
# basic perms for everybody
read-all-process-instances-for-me: read-all-process-instances-for-me:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-instances/for-me/* uri: /process-instances/for-me/*
manage-process-instance-reports: read-process-instance-reports:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/reports/* uri: /process-instances/reports/*
processes-read: processes-read:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/processes uri: /processes
service-tasks:
groups: [everybody]
manage-procurement-admin-instances:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement:*
manage-procurement-admin-instances-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement/*
manage-procurement-admin-instance-logs:
groups: ["Project Lead"]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement:* uri: /service-tasks
manage-procurement-admin-instance-logs-slash: tasks-crud:
groups: ["Project Lead"] groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /tasks/*
user-groups-for-current-user:
groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement/* uri: /user-groups/for-current-user
manage-revenue-streams-instances: manage-revenue-streams-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/* uri: /process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-revenue-streams-instance-logs:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [read]
uri: /v1.0/logs/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances: manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:* uri: /process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-invoice-instance-logs:
groups: ["core-contributor", "demo"]
users: []
allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances: manage-procurement-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:* uri: /process-instances/manage-procurement:vendor-lifecycle-management:*
manage-procurement-instance-logs:
groups: ["core-contributor", "demo"] manage-revenue-streams-instances-for-me:
groups: ["demo"]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement:vendor-lifecycle-management:* uri: /process-instances/for-me/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:vendor-lifecycle-management:*
create-test-instances:
groups: ["test"]
users: []
allowed_permissions: [create, read]
uri: /process-instances/misc:test:*

View File

@ -4,58 +4,52 @@ groups:
admin: admin:
users: users:
[ [
admin, admin@status.im,
jakub, jakub@status.im,
kb, jarrad@status.im,
alex, kb@sartography.com,
dan, alex@sartography.com,
mike, dan@sartography.com,
jason, mike@sartography.com,
j, jason@sartography.com,
jarrad, j@sartography.com,
elizabeth, elizabeth@sartography.com,
jon, jon@sartography.com,
] ]
Finance Team: Finance Team:
users: users:
[ [
jakub, jakub@status.im,
alex, amir@status.im,
dan, jarrad@status.im,
mike, sasha@status.im,
jason, fin@sartography.com,
j, fin1@sartography.com,
amir, alex@sartography.com,
jarrad, dan@sartography.com,
elizabeth, mike@sartography.com,
jon, jason@sartography.com,
sasha, j@sartography.com,
fin, elizabeth@sartography.com,
fin1, jon@sartography.com,
] ]
demo: demo:
users: users:
[ [
core, harmeet@status.im,
fin, sasha@status.im,
fin1, manuchehr@status.im,
harmeet, core@status.im,
sasha, fin@status.im,
manuchehr, fin1@status.im,
lead, lead@status.im,
lead1 lead1@status.im
] ]
core-contributor:
users:
[
core,
harmeet,
]
test: test:
users: [natalia] users: [natalia@sartography.com]
permissions: permissions:
admin: admin:
@ -64,109 +58,91 @@ permissions:
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*
tasks-crud: # open system defaults for everybody
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/service-tasks
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/user-groups/for-current-user
# read all for everybody
read-all-process-groups: read-all-process-groups:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-groups/* uri: /process-groups/*
read-all-process-models: read-all-process-models:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-models/* uri: /process-models/*
# basic perms for everybody
read-all-process-instances-for-me: read-all-process-instances-for-me:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/process-instances/for-me/* uri: /process-instances/for-me/*
read-process-instance-reports: read-process-instance-reports:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/reports/* uri: /process-instances/reports/*
processes-read: processes-read:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [read] allowed_permissions: [read]
uri: /v1.0/processes uri: /processes
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /service-tasks
tasks-crud:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /tasks/*
user-groups-for-current-user:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /user-groups/for-current-user
manage-procurement-admin:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:*
manage-procurement-admin-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement/*
manage-procurement-admin-models:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement:*
manage-procurement-admin-models-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/manage-procurement/*
manage-procurement-admin-instances:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement:*
manage-procurement-admin-instances-slash:
groups: ["Project Lead"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/manage-procurement/*
finance-admin: finance-admin:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:procurement:* uri: /process-groups/manage-procurement:procurement:*
manage-revenue-streams-instances: manage-revenue-streams-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/* uri: /process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances: manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:* uri: /process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances: manage-procurement-instances:
groups: ["core-contributor", "demo"] groups: ["demo"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:* uri: /process-instances/manage-procurement:vendor-lifecycle-management:*
manage-revenue-streams-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances-for-me:
groups: ["demo"]
users: []
allowed_permissions: [read]
uri: /process-instances/for-me/manage-procurement:vendor-lifecycle-management:*
create-test-instances: create-test-instances:
groups: ["test"] groups: ["test"]
users: [] users: []
allowed_permissions: [create, read] allowed_permissions: [create, read]
uri: /v1.0/process-instances/misc:test:* uri: /process-instances/misc:test:*

View File

@ -1,5 +1,12 @@
default_group: everybody default_group: everybody
users:
testadmin1:
service: https://testing/openid/thing
email: testadmin1@spiffworkflow.org
password: admin
preferred_username: El administrador de la muerte
groups: groups:
admin: admin:
users: [testadmin1, testadmin2] users: [testadmin1, testadmin2]
@ -14,7 +21,7 @@ permissions:
admin: admin:
groups: [admin] groups: [admin]
users: [] users: []
allowed_permissions: [create, read, update, delete, list, instantiate] allowed_permissions: [create, read, update, delete]
uri: /* uri: /*
read-all: read-all:
@ -27,29 +34,29 @@ permissions:
groups: [everybody] groups: [everybody]
users: [] users: []
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/* uri: /tasks/*
# TODO: all uris should really have the same structure # TODO: all uris should really have the same structure
finance-admin-group: finance-admin-group:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [testuser4] users: [testuser4]
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/finance/* uri: /process-groups/finance/*
finance-admin-model: finance-admin-model:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [testuser4] users: [testuser4]
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/finance/* uri: /process-models/finance/*
finance-admin-model-lanes: finance-admin-model-lanes:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [testuser4] users: [testuser4]
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/finance:model_with_lanes/* uri: /process-models/finance:model_with_lanes/*
finance-admin-instance-run: finance-admin-instance-run:
groups: ["Finance Team"] groups: ["Finance Team"]
users: [testuser4] users: [testuser4]
allowed_permissions: [create, read, update, delete] allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/* uri: /process-instances/*

View File

@ -0,0 +1,2 @@
"""Api_version."""
V1_API_PATH_PREFIX = "/v1.0"

View File

@ -27,6 +27,9 @@ class GroupModel(FlaskBpmnGroupModel):
identifier = db.Column(db.String(255)) identifier = db.Column(db.String(255))
user_group_assignments = relationship("UserGroupAssignmentModel", cascade="delete") user_group_assignments = relationship("UserGroupAssignmentModel", cascade="delete")
user_group_assignments_waiting = relationship( # type: ignore
"UserGroupAssignmentWaitingModel", cascade="delete"
)
users = relationship( # type: ignore users = relationship( # type: ignore
"UserModel", "UserModel",
viewonly=True, viewonly=True,

View File

@ -32,14 +32,6 @@ class Permission(enum.Enum):
update = "update" update = "update"
delete = "delete" delete = "delete"
# maybe read to GET process_model/process-instances instead?
list = "list"
# maybe use create instead on
# POST http://localhost:7000/v1.0/process-models/category_number_one/call-activity/process-instances/*
# POST http://localhost:7000/v1.0/process-models/category_number_one/call-activity/process-instances/332/run
instantiate = "instantiate" # this is something you do to a process model
class PermissionAssignmentModel(SpiffworkflowBaseDBModel): class PermissionAssignmentModel(SpiffworkflowBaseDBModel):
"""PermissionAssignmentModel.""" """PermissionAssignmentModel."""

View File

@ -1,22 +1,15 @@
"""User.""" """User."""
from __future__ import annotations from __future__ import annotations
from typing import Any
import jwt import jwt
import marshmallow import marshmallow
from flask import current_app from flask import current_app
from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from marshmallow import Schema from marshmallow import Schema
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.orm import validates
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.services.authentication_service import (
AuthenticationProviderTypes,
)
class UserNotFoundError(Exception): class UserNotFoundError(Exception):
@ -28,14 +21,15 @@ class UserModel(SpiffworkflowBaseDBModel):
__tablename__ = "user" __tablename__ = "user"
__table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),) __table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),)
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
# server and service id must be unique, not username. username = db.Column(
username = db.Column(db.String(255), nullable=False, unique=False) db.String(255), nullable=False, unique=True
uid = db.Column(db.String(50), unique=True) ) # should always be a unique value
service = db.Column(db.String(50), nullable=False, unique=False) service = db.Column(
db.String(50), nullable=False, unique=False
) # not 'openid' -- google, aws
service_id = db.Column(db.String(255), nullable=False, unique=False) service_id = db.Column(db.String(255), nullable=False, unique=False)
name = db.Column(db.String(255)) display_name = db.Column(db.String(255))
email = db.Column(db.String(255)) email = db.Column(db.String(255))
updated_at_in_seconds: int = db.Column(db.Integer) updated_at_in_seconds: int = db.Column(db.Integer)
created_at_in_seconds: int = db.Column(db.Integer) created_at_in_seconds: int = db.Column(db.Integer)
@ -49,21 +43,6 @@ class UserModel(SpiffworkflowBaseDBModel):
) )
principal = relationship("PrincipalModel", uselist=False) # type: ignore principal = relationship("PrincipalModel", uselist=False) # type: ignore
@validates("service")
def validate_service(self, key: str, value: Any) -> str:
"""Validate_service."""
try:
ap_type = getattr(AuthenticationProviderTypes, value, None)
except Exception as e:
raise ValueError(f"invalid service type: {value}") from e
if ap_type is not None:
ap_value: str = ap_type.value
return ap_value
raise ApiError(
error_code="invalid_service",
message=f"Could not validate service with value: {value}",
)
def encode_auth_token(self) -> str: def encode_auth_token(self) -> str:
"""Generate the Auth Token. """Generate the Auth Token.

View File

@ -0,0 +1,34 @@
"""UserGroupAssignment."""
from flask_bpmn.models.db import db
from flask_bpmn.models.db import SpiffworkflowBaseDBModel
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship
from spiffworkflow_backend.models.group import GroupModel
class UserGroupAssignmentWaitingModel(SpiffworkflowBaseDBModel):
"""When a user is assigned to a group, but that username does not exist.
We cache it here to be applied in the event the user does log in to the system.
"""
MATCH_ALL_USERS = "*"
__tablename__ = "user_group_assignment_waiting"
__table_args__ = (
db.UniqueConstraint(
"username", "group_id", name="user_group_assignment_staged_unique"
),
)
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(255), nullable=False)
group_id = db.Column(ForeignKey(GroupModel.id), nullable=False)
group = relationship("GroupModel", overlaps="groups,user_group_assignments_waiting,users") # type: ignore
def is_match_all(self) -> bool:
"""Is_match_all."""
if self.username == self.MATCH_ALL_USERS:
return True
return False

View File

@ -141,7 +141,7 @@ def process_model_save(process_model_id: str, file_name: str) -> Union[str, Resp
@admin_blueprint.route("/process-models/<process_model_id>/run", methods=["GET"]) @admin_blueprint.route("/process-models/<process_model_id>/run", methods=["GET"])
def process_model_run(process_model_id: str) -> Union[str, Response]: def process_model_run(process_model_id: str) -> Union[str, Response]:
"""Process_model_run.""" """Process_model_run."""
user = UserService.create_user("internal", "Mr. Test", username="Mr. Test") user = UserService.create_user("Mr. Test", "internal", "Mr. Test")
process_instance = ( process_instance = (
ProcessInstanceService.create_process_instance_from_process_model_identifier( ProcessInstanceService.create_process_instance_from_process_model_identifier(
process_model_id, user process_model_id, user

View File

@ -111,6 +111,7 @@ def token() -> dict:
"iat": time.time(), "iat": time.time(),
"exp": time.time() + 86400, # Expire after a day. "exp": time.time() + 86400, # Expire after a day.
"sub": user_name, "sub": user_name,
"email": user_details["email"],
"preferred_username": user_details.get("preferred_username", user_name), "preferred_username": user_details.get("preferred_username", user_name),
}, },
client_secret, client_secret,

View File

@ -76,7 +76,7 @@ def verify_token(
except ApiError as ae: # API Error is only thrown in the token is outdated. except ApiError as ae: # API Error is only thrown in the token is outdated.
# Try to refresh the token # Try to refresh the token
user = UserService.get_user_by_service_and_service_id( user = UserService.get_user_by_service_and_service_id(
"open_id", decoded_token["sub"] decoded_token["iss"], decoded_token["sub"]
) )
if user: if user:
refresh_token = AuthenticationService.get_refresh_token(user.id) refresh_token = AuthenticationService.get_refresh_token(user.id)
@ -105,10 +105,12 @@ def verify_token(
) from e ) from e
if ( if (
user_info is not None and "error" not in user_info user_info is not None
and "error" not in user_info
and "iss" in user_info
): # not sure what to test yet ): # not sure what to test yet
user_model = ( user_model = (
UserModel.query.filter(UserModel.service == "open_id") UserModel.query.filter(UserModel.service == user_info["iss"])
.filter(UserModel.service_id == user_info["sub"]) .filter(UserModel.service_id == user_info["sub"])
.first() .first()
) )
@ -341,9 +343,5 @@ def get_user_from_decoded_internal_token(decoded_token: dict) -> Optional[UserMo
) )
if user: if user:
return user return user
user = UserModel( user = UserService.create_user(service_id, service, service_id)
username=service_id,
service=service,
service_id=service_id,
)
return user return user

View File

@ -1,43 +0,0 @@
"""Get_env."""
from typing import Any
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.group import GroupNotFoundError
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user import UserNotFoundError
from spiffworkflow_backend.scripts.script import Script
from spiffworkflow_backend.services.user_service import UserService
class AddUserToGroup(Script):
"""AddUserToGroup."""
def get_description(self) -> str:
"""Get_description."""
return """Add a given user to a given group."""
def run(
self,
script_attributes_context: ScriptAttributesContext,
*args: Any,
**kwargs: Any,
) -> Any:
"""Run."""
username = args[0]
group_identifier = args[1]
user = UserModel.query.filter_by(username=username).first()
if user is None:
raise UserNotFoundError(
f"Script 'add_user_to_group' could not find a user with username: {username}"
)
group = GroupModel.query.filter_by(identifier=group_identifier).first()
if group is None:
raise GroupNotFoundError(
f"Script 'add_user_to_group' could not find group with identifier '{group_identifier}'."
)
UserService.add_user_to_group(user, group)

View File

@ -10,6 +10,11 @@ from spiffworkflow_backend.scripts.script import Script
class FactService(Script): class FactService(Script):
"""FactService.""" """FactService."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Just your basic class that can pull in data from a few api endpoints and return """Just your basic class that can pull in data from a few api endpoints and

View File

@ -0,0 +1,56 @@
"""Get_env."""
from collections import OrderedDict
from typing import Any
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel
from spiffworkflow_backend.models.permission_target import PermissionTargetModel
from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.scripts.script import Script
class GetAllPermissions(Script):
"""GetAllPermissions."""
def get_description(self) -> str:
"""Get_description."""
return """Get all permissions currently in the system."""
def run(
self,
script_attributes_context: ScriptAttributesContext,
*args: Any,
**kwargs: Any,
) -> Any:
"""Run."""
permission_assignments = (
PermissionAssignmentModel.query.join(
PrincipalModel,
PrincipalModel.id == PermissionAssignmentModel.principal_id,
)
.join(GroupModel, GroupModel.id == PrincipalModel.group_id)
.join(
PermissionTargetModel,
PermissionTargetModel.id
== PermissionAssignmentModel.permission_target_id,
)
.add_columns(
PermissionAssignmentModel.permission,
PermissionTargetModel.uri,
GroupModel.identifier.label("group_identifier"),
)
)
permissions: OrderedDict[tuple[str, str], list[str]] = OrderedDict()
for pa in permission_assignments:
permissions.setdefault((pa.group_identifier, pa.uri), []).append(
pa.permission
)
return [
{"group_identifier": k[0], "uri": k[1], "permissions": sorted(v)}
for k, v in permissions.items()
]

View File

@ -12,6 +12,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetCurrentUser(Script): class GetCurrentUser(Script):
"""GetCurrentUser.""" """GetCurrentUser."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Return the current user.""" return """Return the current user."""

View File

@ -10,6 +10,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetEnv(Script): class GetEnv(Script):
"""GetEnv.""" """GetEnv."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Returns the current environment - ie testing, staging, production.""" return """Returns the current environment - ie testing, staging, production."""

View File

@ -12,6 +12,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetFrontendUrl(Script): class GetFrontendUrl(Script):
"""GetFrontendUrl.""" """GetFrontendUrl."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Return the url to the frontend.""" return """Return the url to the frontend."""

View File

@ -12,6 +12,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetGroupMembers(Script): class GetGroupMembers(Script):
"""GetGroupMembers.""" """GetGroupMembers."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Return the list of usernames of the users in the given group.""" return """Return the list of usernames of the users in the given group."""

View File

@ -14,6 +14,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetLocaltime(Script): class GetLocaltime(Script):
"""GetLocaltime.""" """GetLocaltime."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Converts a Datetime object into a Datetime object for a specific timezone. return """Converts a Datetime object into a Datetime object for a specific timezone.

View File

@ -10,6 +10,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetProcessInfo(Script): class GetProcessInfo(Script):
"""GetProcessInfo.""" """GetProcessInfo."""
@staticmethod
def requires_privileged_permissions() -> bool:
"""We have deemed this function safe to run without elevated permissions."""
return False
def get_description(self) -> str: def get_description(self) -> str:
"""Get_description.""" """Get_description."""
return """Returns a dictionary of information about the currently running process.""" return """Returns a dictionary of information about the currently running process."""

View File

@ -0,0 +1,39 @@
"""Get_env."""
from typing import Any
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.scripts.script import Script
from spiffworkflow_backend.services.authorization_service import AuthorizationService
class RecreatePermissions(Script):
"""RecreatePermissions."""
def get_description(self) -> str:
"""Get_description."""
return """Add permissions using a dict.
group_info: [
{
'name': group_identifier,
'users': array_of_users,
'permissions': [
{
'actions': array_of_actions - create, read, etc,
'uri': target_uri
}
]
}
]
"""
def run(
self,
script_attributes_context: ScriptAttributesContext,
*args: Any,
**kwargs: Any,
) -> Any:
"""Run."""
group_info = args[0]
AuthorizationService.refresh_permissions(group_info)

View File

@ -10,9 +10,12 @@ from typing import Callable
from flask_bpmn.api.api_error import ApiError from flask_bpmn.api.api_error import ApiError
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceNotFoundError
from spiffworkflow_backend.models.script_attributes_context import ( from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext, ScriptAttributesContext,
) )
from spiffworkflow_backend.services.authorization_service import AuthorizationService
# Generally speaking, having some global in a flask app is TERRIBLE. # Generally speaking, having some global in a flask app is TERRIBLE.
# This is here, because after loading the application this will never change under # This is here, because after loading the application this will never change under
@ -20,6 +23,10 @@ from spiffworkflow_backend.models.script_attributes_context import (
SCRIPT_SUB_CLASSES = None SCRIPT_SUB_CLASSES = None
class ScriptUnauthorizedForUserError(Exception):
"""ScriptUnauthorizedForUserError."""
class Script: class Script:
"""Provides an abstract class that defines how scripts should work, this must be extended in all Script Tasks.""" """Provides an abstract class that defines how scripts should work, this must be extended in all Script Tasks."""
@ -43,6 +50,15 @@ class Script:
+ "does not properly implement the run function.", + "does not properly implement the run function.",
) )
@staticmethod
def requires_privileged_permissions() -> bool:
"""It seems safer to default to True and make safe functions opt in for any user to run them.
To give access to script for a given user, add a 'create' permission with following target-uri:
'/can-run-privileged-script/{script_name}'
"""
return True
@staticmethod @staticmethod
def generate_augmented_list( def generate_augmented_list(
script_attributes_context: ScriptAttributesContext, script_attributes_context: ScriptAttributesContext,
@ -71,18 +87,50 @@ class Script:
that we created. that we created.
""" """
instance = subclass() instance = subclass()
return lambda *ar, **kw: subclass.run(
instance, def check_script_permission() -> None:
script_attributes_context, """Check_script_permission."""
*ar, if subclass.requires_privileged_permissions():
**kw, script_function_name = get_script_function_name(subclass)
) uri = f"/can-run-privileged-script/{script_function_name}"
process_instance = ProcessInstanceModel.query.filter_by(
id=script_attributes_context.process_instance_id
).first()
if process_instance is None:
raise ProcessInstanceNotFoundError(
f"Could not find a process instance with id '{script_attributes_context.process_instance_id}' "
f"when running script '{script_function_name}'"
)
user = process_instance.process_initiator
has_permission = AuthorizationService.user_has_permission(
user=user, permission="create", target_uri=uri
)
if not has_permission:
raise ScriptUnauthorizedForUserError(
f"User {user.username} does not have access to run privileged script '{script_function_name}'"
)
def run_script_if_allowed(*ar: Any, **kw: Any) -> Any:
"""Run_script_if_allowed."""
check_script_permission()
return subclass.run(
instance,
script_attributes_context,
*ar,
**kw,
)
return run_script_if_allowed
def get_script_function_name(subclass: type[Script]) -> str:
"""Get_script_function_name."""
return subclass.__module__.split(".")[-1]
execlist = {} execlist = {}
subclasses = Script.get_all_subclasses() subclasses = Script.get_all_subclasses()
for x in range(len(subclasses)): for x in range(len(subclasses)):
subclass = subclasses[x] subclass = subclasses[x]
execlist[subclass.__module__.split(".")[-1]] = make_closure( execlist[get_script_function_name(subclass)] = make_closure(
subclass, script_attributes_context=script_attributes_context subclass, script_attributes_context=script_attributes_context
) )
return execlist return execlist

View File

@ -93,7 +93,7 @@ class AuthenticationService:
+ f"?state={state}&" + f"?state={state}&"
+ "response_type=code&" + "response_type=code&"
+ f"client_id={self.client_id()}&" + f"client_id={self.client_id()}&"
+ "scope=openid&" + "scope=openid profile email&"
+ f"redirect_uri={return_redirect_url}" + f"redirect_uri={return_redirect_url}"
) )
return login_redirect_url return login_redirect_url

View File

@ -1,10 +1,14 @@
"""Authorization_service.""" """Authorization_service."""
import inspect import inspect
import re import re
from dataclasses import dataclass
from hashlib import sha256 from hashlib import sha256
from hmac import compare_digest from hmac import compare_digest
from hmac import HMAC from hmac import HMAC
from typing import Any
from typing import Optional from typing import Optional
from typing import Set
from typing import TypedDict
from typing import Union from typing import Union
import jwt import jwt
@ -19,6 +23,7 @@ from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy import text from sqlalchemy import text
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.human_task import HumanTaskModel from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel
@ -45,6 +50,34 @@ class UserDoesNotHaveAccessToTaskError(Exception):
"""UserDoesNotHaveAccessToTaskError.""" """UserDoesNotHaveAccessToTaskError."""
class InvalidPermissionError(Exception):
"""InvalidPermissionError."""
@dataclass
class PermissionToAssign:
"""PermissionToAssign."""
permission: str
target_uri: str
PATH_SEGMENTS_FOR_PERMISSION_ALL = [
"/logs",
"/process-instances",
"/process-instance-suspend",
"/process-instance-terminate",
"/task-data",
]
class DesiredPermissionDict(TypedDict):
"""DesiredPermissionDict."""
group_identifiers: Set[str]
permission_assignments: list[PermissionAssignmentModel]
class AuthorizationService: class AuthorizationService:
"""Determine whether a user has permission to perform their request.""" """Determine whether a user has permission to perform their request."""
@ -75,6 +108,7 @@ class AuthorizationService:
) -> bool: ) -> bool:
"""Has_permission.""" """Has_permission."""
principal_ids = [p.id for p in principals] principal_ids = [p.id for p in principals]
target_uri_normalized = target_uri.removeprefix(V1_API_PATH_PREFIX)
permission_assignments = ( permission_assignments = (
PermissionAssignmentModel.query.filter( PermissionAssignmentModel.query.filter(
@ -84,10 +118,12 @@ class AuthorizationService:
.join(PermissionTargetModel) .join(PermissionTargetModel)
.filter( .filter(
or_( or_(
text(f"'{target_uri}' LIKE permission_target.uri"), text(f"'{target_uri_normalized}' LIKE permission_target.uri"),
# to check for exact matches as well # to check for exact matches as well
# see test_user_can_access_base_path_when_given_wildcard_permission unit test # see test_user_can_access_base_path_when_given_wildcard_permission unit test
text(f"'{target_uri}' = replace(permission_target.uri, '/%', '')"), text(
f"'{target_uri_normalized}' = replace(replace(permission_target.uri, '/%', ''), ':%', '')"
),
) )
) )
.all() .all()
@ -127,17 +163,15 @@ class AuthorizationService:
return cls.has_permission(principals, permission, target_uri) return cls.has_permission(principals, permission, target_uri)
@classmethod @classmethod
def delete_all_permissions_and_recreate(cls) -> None: def delete_all_permissions(cls) -> None:
"""Delete_all_permissions_and_recreate.""" """Delete_all_permissions_and_recreate. EXCEPT For permissions for the current user?"""
for model in [PermissionAssignmentModel, PermissionTargetModel]: for model in [PermissionAssignmentModel, PermissionTargetModel]:
db.session.query(model).delete() db.session.query(model).delete()
# cascading to principals doesn't seem to work when attempting to delete all so do it like this instead # cascading to principals doesn't seem to work when attempting to delete all so do it like this instead
for group in GroupModel.query.all(): for group in GroupModel.query.all():
db.session.delete(group) db.session.delete(group)
db.session.commit() db.session.commit()
cls.import_permissions_from_yaml_file()
@classmethod @classmethod
def associate_user_with_group(cls, user: UserModel, group: GroupModel) -> None: def associate_user_with_group(cls, user: UserModel, group: GroupModel) -> None:
@ -155,7 +189,7 @@ class AuthorizationService:
@classmethod @classmethod
def import_permissions_from_yaml_file( def import_permissions_from_yaml_file(
cls, raise_if_missing_user: bool = False cls, raise_if_missing_user: bool = False
) -> None: ) -> DesiredPermissionDict:
"""Import_permissions_from_yaml_file.""" """Import_permissions_from_yaml_file."""
if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None: if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None:
raise ( raise (
@ -169,13 +203,16 @@ class AuthorizationService:
permission_configs = yaml.safe_load(file) permission_configs = yaml.safe_load(file)
default_group = None default_group = None
unique_user_group_identifiers: Set[str] = set()
if "default_group" in permission_configs: if "default_group" in permission_configs:
default_group_identifier = permission_configs["default_group"] default_group_identifier = permission_configs["default_group"]
default_group = GroupService.find_or_create_group(default_group_identifier) default_group = GroupService.find_or_create_group(default_group_identifier)
unique_user_group_identifiers.add(default_group_identifier)
if "groups" in permission_configs: if "groups" in permission_configs:
for group_identifier, group_config in permission_configs["groups"].items(): for group_identifier, group_config in permission_configs["groups"].items():
group = GroupService.find_or_create_group(group_identifier) group = GroupService.find_or_create_group(group_identifier)
unique_user_group_identifiers.add(group_identifier)
for username in group_config["users"]: for username in group_config["users"]:
user = UserModel.query.filter_by(username=username).first() user = UserModel.query.filter_by(username=username).first()
if user is None: if user is None:
@ -188,26 +225,25 @@ class AuthorizationService:
continue continue
cls.associate_user_with_group(user, group) cls.associate_user_with_group(user, group)
permission_assignments = []
if "permissions" in permission_configs: if "permissions" in permission_configs:
for _permission_identifier, permission_config in permission_configs[ for _permission_identifier, permission_config in permission_configs[
"permissions" "permissions"
].items(): ].items():
uri = permission_config["uri"] uri = permission_config["uri"]
uri_with_percent = re.sub(r"\*", "%", uri) permission_target = cls.find_or_create_permission_target(uri)
permission_target = PermissionTargetModel.query.filter_by(
uri=uri_with_percent
).first()
if permission_target is None:
permission_target = PermissionTargetModel(uri=uri_with_percent)
db.session.add(permission_target)
db.session.commit()
for allowed_permission in permission_config["allowed_permissions"]: for allowed_permission in permission_config["allowed_permissions"]:
if "groups" in permission_config: if "groups" in permission_config:
for group_identifier in permission_config["groups"]: for group_identifier in permission_config["groups"]:
group = GroupService.find_or_create_group(group_identifier) group = GroupService.find_or_create_group(group_identifier)
cls.create_permission_for_principal( unique_user_group_identifiers.add(group_identifier)
group.principal, permission_target, allowed_permission permission_assignments.append(
cls.create_permission_for_principal(
group.principal,
permission_target,
allowed_permission,
)
) )
if "users" in permission_config: if "users" in permission_config:
for username in permission_config["users"]: for username in permission_config["users"]:
@ -218,14 +254,35 @@ class AuthorizationService:
.filter(UserModel.username == username) .filter(UserModel.username == username)
.first() .first()
) )
cls.create_permission_for_principal( permission_assignments.append(
principal, permission_target, allowed_permission cls.create_permission_for_principal(
principal, permission_target, allowed_permission
)
) )
if default_group is not None: if default_group is not None:
for user in UserModel.query.all(): for user in UserModel.query.all():
cls.associate_user_with_group(user, default_group) cls.associate_user_with_group(user, default_group)
return {
"group_identifiers": unique_user_group_identifiers,
"permission_assignments": permission_assignments,
}
@classmethod
def find_or_create_permission_target(cls, uri: str) -> PermissionTargetModel:
"""Find_or_create_permission_target."""
uri_with_percent = re.sub(r"\*", "%", uri)
target_uri_normalized = uri_with_percent.removeprefix(V1_API_PATH_PREFIX)
permission_target: Optional[
PermissionTargetModel
] = PermissionTargetModel.query.filter_by(uri=target_uri_normalized).first()
if permission_target is None:
permission_target = PermissionTargetModel(uri=target_uri_normalized)
db.session.add(permission_target)
db.session.commit()
return permission_target
@classmethod @classmethod
def create_permission_for_principal( def create_permission_for_principal(
cls, cls,
@ -449,33 +506,48 @@ class AuthorizationService:
@classmethod @classmethod
def create_user_from_sign_in(cls, user_info: dict) -> UserModel: def create_user_from_sign_in(cls, user_info: dict) -> UserModel:
"""Create_user_from_sign_in.""" """Create_user_from_sign_in."""
"""Name, family_name, given_name, middle_name, nickname, preferred_username,"""
"""Profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at. """
"""Email."""
is_new_user = False is_new_user = False
user_model = ( user_model = (
UserModel.query.filter(UserModel.service == "open_id") UserModel.query.filter(UserModel.service == user_info["iss"])
.filter(UserModel.service_id == user_info["sub"]) .filter(UserModel.service_id == user_info["sub"])
.first() .first()
) )
email = display_name = username = ""
if "email" in user_info:
username = user_info["email"]
email = user_info["email"]
else: # we fall back to the sub, which may be very ugly.
username = user_info["sub"] + "@" + user_info["iss"]
if "preferred_username" in user_info:
display_name = user_info["preferred_username"]
elif "nickname" in user_info:
display_name = user_info["nickname"]
elif "name" in user_info:
display_name = user_info["name"]
if user_model is None: if user_model is None:
current_app.logger.debug("create_user in login_return") current_app.logger.debug("create_user in login_return")
is_new_user = True is_new_user = True
name = username = email = ""
if "name" in user_info:
name = user_info["name"]
if "username" in user_info:
username = user_info["username"]
elif "preferred_username" in user_info:
username = user_info["preferred_username"]
if "email" in user_info:
email = user_info["email"]
user_model = UserService().create_user( user_model = UserService().create_user(
service="open_id",
service_id=user_info["sub"],
name=name,
username=username, username=username,
service=user_info["iss"],
service_id=user_info["sub"],
email=email, email=email,
display_name=display_name,
) )
else:
# Update with the latest information
user_model.username = username
user_model.email = email
user_model.display_name = display_name
user_model.service = user_info["iss"]
user_model.service_id = user_info["sub"]
# this may eventually get too slow. # this may eventually get too slow.
# when it does, be careful about backgrounding, because # when it does, be careful about backgrounding, because
# the user will immediately need permissions to use the site. # the user will immediately need permissions to use the site.
@ -490,6 +562,212 @@ class AuthorizationService:
# this cannot be None so ignore mypy # this cannot be None so ignore mypy
return user_model # type: ignore return user_model # type: ignore
@classmethod
def get_permissions_to_assign(
cls,
permission_set: str,
process_related_path_segment: str,
target_uris: list[str],
) -> list[PermissionToAssign]:
"""Get_permissions_to_assign."""
permissions = permission_set.split(",")
if permission_set == "all":
permissions = ["create", "read", "update", "delete"]
permissions_to_assign: list[PermissionToAssign] = []
# we were thinking that if you can start an instance, you ought to be able to view your own instances.
if permission_set == "start":
target_uri = f"/process-instances/{process_related_path_segment}"
permissions_to_assign.append(
PermissionToAssign(permission="create", target_uri=target_uri)
)
target_uri = f"/process-instances/for-me/{process_related_path_segment}"
permissions_to_assign.append(
PermissionToAssign(permission="read", target_uri=target_uri)
)
else:
if permission_set == "all":
for path_segment in PATH_SEGMENTS_FOR_PERMISSION_ALL:
target_uris.append(f"{path_segment}/{process_related_path_segment}")
for target_uri in target_uris:
for permission in permissions:
permissions_to_assign.append(
PermissionToAssign(permission=permission, target_uri=target_uri)
)
return permissions_to_assign
@classmethod
def explode_permissions(
cls, permission_set: str, target: str
) -> list[PermissionToAssign]:
"""Explodes given permissions to and returns list of PermissionToAssign objects.
These can be used to then iterate through and inserted into the database.
Target Macros:
ALL
* gives access to ALL api endpoints - useful to give admin-like permissions
PG:[process_group_identifier]
* affects given process-group and all sub process-groups and process-models
PM:[process_model_identifier]
* affects given process-model
BASIC
* Basic access to complete tasks and use the site
Permission Macros:
all
* create, read, update, delete
start
* create process-instances (aka instantiate or start a process-model)
* only works with PG and PM target macros
"""
permissions_to_assign: list[PermissionToAssign] = []
permissions = permission_set.split(",")
if permission_set == "all":
permissions = ["create", "read", "update", "delete"]
if target.startswith("PG:"):
process_group_identifier = (
target.removeprefix("PG:").replace("/", ":").removeprefix(":")
)
process_related_path_segment = f"{process_group_identifier}:*"
if process_group_identifier == "ALL":
process_related_path_segment = "*"
target_uris = [
f"/process-groups/{process_related_path_segment}",
f"/process-models/{process_related_path_segment}",
]
permissions_to_assign = (
permissions_to_assign
+ cls.get_permissions_to_assign(
permission_set, process_related_path_segment, target_uris
)
)
elif target.startswith("PM:"):
process_model_identifier = (
target.removeprefix("PM:").replace("/", ":").removeprefix(":")
)
process_related_path_segment = f"{process_model_identifier}/*"
if process_model_identifier == "ALL":
process_related_path_segment = "*"
target_uris = [f"/process-models/{process_related_path_segment}"]
permissions_to_assign = (
permissions_to_assign
+ cls.get_permissions_to_assign(
permission_set, process_related_path_segment, target_uris
)
)
elif permission_set == "start":
raise InvalidPermissionError(
"Permission 'start' is only available for macros PM and PG."
)
elif target.startswith("BASIC"):
permissions_to_assign.append(
PermissionToAssign(
permission="read", target_uri="/process-instances/for-me"
)
)
permissions_to_assign.append(
PermissionToAssign(permission="read", target_uri="/processes")
)
permissions_to_assign.append(
PermissionToAssign(permission="read", target_uri="/service-tasks")
)
permissions_to_assign.append(
PermissionToAssign(
permission="read", target_uri="/user-groups/for-current-user"
)
)
for permission in ["create", "read", "update", "delete"]:
permissions_to_assign.append(
PermissionToAssign(
permission=permission, target_uri="/process-instances/reports/*"
)
)
permissions_to_assign.append(
PermissionToAssign(permission=permission, target_uri="/tasks/*")
)
elif target == "ALL":
for permission in permissions:
permissions_to_assign.append(
PermissionToAssign(permission=permission, target_uri="/*")
)
elif target.startswith("/"):
for permission in permissions:
permissions_to_assign.append(
PermissionToAssign(permission=permission, target_uri=target)
)
else:
raise InvalidPermissionError(
f"Target uri '{target}' with permission set '{permission_set}' is invalid. "
f"The target uri must either be a macro of PG, PM, BASIC, or ALL or an api uri."
)
return permissions_to_assign
@classmethod
def add_permission_from_uri_or_macro(
cls, group_identifier: str, permission: str, target: str
) -> list[PermissionAssignmentModel]:
"""Add_permission_from_uri_or_macro."""
group = GroupService.find_or_create_group(group_identifier)
permissions_to_assign = cls.explode_permissions(permission, target)
permission_assignments = []
for permission_to_assign in permissions_to_assign:
permission_target = cls.find_or_create_permission_target(
permission_to_assign.target_uri
)
permission_assignments.append(
cls.create_permission_for_principal(
group.principal, permission_target, permission_to_assign.permission
)
)
return permission_assignments
@classmethod
def refresh_permissions(cls, group_info: list[dict[str, Any]]) -> None:
"""Adds new permission assignments and deletes old ones."""
initial_permission_assignments = PermissionAssignmentModel.query.all()
result = cls.import_permissions_from_yaml_file()
desired_permission_assignments = result["permission_assignments"]
desired_group_identifiers = result["group_identifiers"]
for group in group_info:
for username in group["users"]:
GroupService.add_user_to_group_or_add_to_waiting(
username, group["name"]
)
for permission in group["permissions"]:
for crud_op in permission["actions"]:
desired_permission_assignments.extend(
cls.add_permission_from_uri_or_macro(
group_identifier=group["name"],
target=permission["uri"],
permission=crud_op,
)
)
desired_group_identifiers.add(group["name"])
for ipa in initial_permission_assignments:
if ipa not in desired_permission_assignments:
db.session.delete(ipa)
groups_to_delete = GroupModel.query.filter(
GroupModel.identifier.not_in(desired_group_identifiers)
).all()
for gtd in groups_to_delete:
db.session.delete(gtd)
db.session.commit()
class KeycloakAuthorization: class KeycloakAuthorization:
"""Interface with Keycloak server.""" """Interface with Keycloak server."""

View File

@ -4,6 +4,7 @@ from typing import Optional
from flask_bpmn.models.db import db from flask_bpmn.models.db import db
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.user_service import UserService from spiffworkflow_backend.services.user_service import UserService
@ -22,3 +23,15 @@ class GroupService:
db.session.commit() db.session.commit()
UserService.create_principal(group.id, id_column_name="group_id") UserService.create_principal(group.id, id_column_name="group_id")
return group return group
@classmethod
def add_user_to_group_or_add_to_waiting(
cls, username: str, group_identifier: str
) -> None:
"""Add_user_to_group_or_add_to_waiting."""
group = cls.find_or_create_group(group_identifier)
user = UserModel.query.filter_by(username=username).first()
if user:
UserService.add_user_to_group(user, group)
else:
UserService.add_waiting_group_assignment(username, group)

View File

@ -151,6 +151,7 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
"time": time, "time": time,
"decimal": decimal, "decimal": decimal,
"_strptime": _strptime, "_strptime": _strptime,
"enumerate": enumerate,
} }
# This will overwrite the standard builtins # This will overwrite the standard builtins

View File

@ -414,13 +414,16 @@ class ProcessInstanceReportService:
) )
if report_filter.with_tasks_assigned_to_my_group is True: if report_filter.with_tasks_assigned_to_my_group is True:
group_model_join_conditions = [GroupModel.id == HumanTaskModel.lane_assignment_id] group_model_join_conditions = [
GroupModel.id == HumanTaskModel.lane_assignment_id
]
if report_filter.user_group_identifier: if report_filter.user_group_identifier:
group_model_join_conditions.append(GroupModel.identifier == report_filter.user_group_identifier) group_model_join_conditions.append(
GroupModel.identifier == report_filter.user_group_identifier
)
process_instance_query = process_instance_query.join(HumanTaskModel) process_instance_query = process_instance_query.join(HumanTaskModel)
process_instance_query = process_instance_query.join( process_instance_query = process_instance_query.join(
GroupModel, GroupModel, and_(*group_model_join_conditions)
and_(*group_model_join_conditions)
) )
process_instance_query = process_instance_query.join( process_instance_query = process_instance_query.join(
UserGroupAssignmentModel, UserGroupAssignmentModel,

View File

@ -17,7 +17,8 @@ from spiffworkflow_backend.models.task import MultiInstanceType
from spiffworkflow_backend.models.task import Task from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.git_service import GitService, GitCommandError from spiffworkflow_backend.services.git_service import GitCommandError
from spiffworkflow_backend.services.git_service import GitService
from spiffworkflow_backend.services.process_instance_processor import ( from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor, ProcessInstanceProcessor,
) )
@ -38,7 +39,7 @@ class ProcessInstanceService:
"""Get_process_instance_from_spec.""" """Get_process_instance_from_spec."""
try: try:
current_git_revision = GitService.get_current_revision() current_git_revision = GitService.get_current_revision()
except GitCommandError as ge: except GitCommandError:
current_git_revision = "" current_git_revision = ""
process_instance_model = ProcessInstanceModel( process_instance_model = ProcessInstanceModel(
status=ProcessInstanceStatus.not_started.value, status=ProcessInstanceStatus.not_started.value,

View File

@ -224,10 +224,10 @@ class ProcessModelService(FileSystemService):
new_process_model_list = [] new_process_model_list = []
for process_model in process_models: for process_model in process_models:
uri = f"/v1.0/process-instances/{process_model.id.replace('/', ':')}" uri = f"/v1.0/process-instances/{process_model.id.replace('/', ':')}"
result = AuthorizationService.user_has_permission( has_permission = AuthorizationService.user_has_permission(
user=user, permission="create", target_uri=uri user=user, permission="create", target_uri=uri
) )
if result: if has_permission:
new_process_model_list.append(process_model) new_process_model_list.append(process_model)
return new_process_model_list return new_process_model_list

View File

@ -13,6 +13,9 @@ from spiffworkflow_backend.models.human_task_user import HumanTaskUserModel
from spiffworkflow_backend.models.principal import PrincipalModel from spiffworkflow_backend.models.principal import PrincipalModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
from spiffworkflow_backend.models.user_group_assignment_waiting import (
UserGroupAssignmentWaitingModel,
)
class UserService: class UserService:
@ -21,11 +24,11 @@ class UserService:
@classmethod @classmethod
def create_user( def create_user(
cls, cls,
username: str,
service: str, service: str,
service_id: str, service_id: str,
name: Optional[str] = "",
username: Optional[str] = "",
email: Optional[str] = "", email: Optional[str] = "",
display_name: Optional[str] = "",
) -> UserModel: ) -> UserModel:
"""Create_user.""" """Create_user."""
user_model: Optional[UserModel] = ( user_model: Optional[UserModel] = (
@ -41,8 +44,8 @@ class UserService:
username=username, username=username,
service=service, service=service,
service_id=service_id, service_id=service_id,
name=name,
email=email, email=email,
display_name=display_name,
) )
db.session.add(user_model) db.session.add(user_model)
@ -55,6 +58,7 @@ class UserService:
message=f"Could not add user {username}", message=f"Could not add user {username}",
) from e ) from e
cls.create_principal(user_model.id) cls.create_principal(user_model.id)
UserService().apply_waiting_group_assignments(user_model)
return user_model return user_model
else: else:
@ -69,45 +73,12 @@ class UserService:
) )
) )
@classmethod
def find_or_create_user(
cls,
service: str,
service_id: str,
name: Optional[str] = None,
username: Optional[str] = None,
email: Optional[str] = None,
) -> UserModel:
"""Find_or_create_user."""
user_model: UserModel
try:
user_model = cls.create_user(
service=service,
service_id=service_id,
name=name,
username=username,
email=email,
)
except ApiError:
user_model = (
UserModel.query.filter(UserModel.service == service)
.filter(UserModel.service_id == service_id)
.first()
)
return user_model
# Returns true if the current user is logged in. # Returns true if the current user is logged in.
@staticmethod @staticmethod
def has_user() -> bool: def has_user() -> bool:
"""Has_user.""" """Has_user."""
return "token" in g and bool(g.token) and "user" in g and bool(g.user) return "token" in g and bool(g.token) and "user" in g and bool(g.user)
# Returns true if the given user uid is different from the current user's uid.
@staticmethod
def is_different_user(uid: str) -> bool:
"""Is_different_user."""
return UserService.has_user() and uid is not None and uid is not g.user.uid
@staticmethod @staticmethod
def current_user() -> Any: def current_user() -> Any:
"""Current_user.""" """Current_user."""
@ -117,20 +88,6 @@ class UserService:
) )
return g.user return g.user
@staticmethod
def in_list(uids: list[str]) -> bool:
"""Returns true if the current user's id is in the given list of ids.
False if there is no user, or the user is not in the list.
"""
if (
UserService.has_user()
): # If someone is logged in, lock tasks that don't belong to them.
user = UserService.current_user()
if user.uid in uids:
return True
return False
@staticmethod @staticmethod
def get_principal_by_user_id(user_id: int) -> PrincipalModel: def get_principal_by_user_id(user_id: int) -> PrincipalModel:
"""Get_principal_by_user_id.""" """Get_principal_by_user_id."""
@ -173,8 +130,57 @@ class UserService:
@classmethod @classmethod
def add_user_to_group(cls, user: UserModel, group: GroupModel) -> None: def add_user_to_group(cls, user: UserModel, group: GroupModel) -> None:
"""Add_user_to_group.""" """Add_user_to_group."""
ugam = UserGroupAssignmentModel(user_id=user.id, group_id=group.id) exists = (
db.session.add(ugam) UserGroupAssignmentModel()
.query.filter_by(user_id=user.id)
.filter_by(group_id=group.id)
.count()
)
if not exists:
ugam = UserGroupAssignmentModel(user_id=user.id, group_id=group.id)
db.session.add(ugam)
db.session.commit()
@classmethod
def add_waiting_group_assignment(cls, username: str, group: GroupModel) -> None:
"""Add_waiting_group_assignment."""
wugam = (
UserGroupAssignmentWaitingModel()
.query.filter_by(username=username)
.filter_by(group_id=group.id)
.first()
)
if not wugam:
wugam = UserGroupAssignmentWaitingModel(
username=username, group_id=group.id
)
db.session.add(wugam)
db.session.commit()
if wugam.is_match_all():
for user in UserModel.query.all():
cls.add_user_to_group(user, group)
@classmethod
def apply_waiting_group_assignments(cls, user: UserModel) -> None:
"""Apply_waiting_group_assignments."""
waiting = (
UserGroupAssignmentWaitingModel()
.query.filter(UserGroupAssignmentWaitingModel.username == user.username)
.all()
)
for assignment in waiting:
cls.add_user_to_group(user, assignment.group)
db.session.delete(assignment)
wildcard = (
UserGroupAssignmentWaitingModel()
.query.filter(
UserGroupAssignmentWaitingModel.username
== UserGroupAssignmentWaitingModel.MATCH_ALL_USERS
)
.all()
)
for assignment in wildcard:
cls.add_user_to_group(user, assignment.group)
db.session.commit() db.session.commit()
@staticmethod @staticmethod

View File

@ -41,7 +41,7 @@ class BaseTest:
if isinstance(user, UserModel): if isinstance(user, UserModel):
return user return user
user = UserService.create_user("internal", username, username=username) user = UserService.create_user(username, "internal", username)
if isinstance(user, UserModel): if isinstance(user, UserModel):
return user return user
@ -324,13 +324,9 @@ class BaseTest:
permission_names: Optional[list[str]] = None, permission_names: Optional[list[str]] = None,
) -> UserModel: ) -> UserModel:
"""Add_permissions_to_user.""" """Add_permissions_to_user."""
permission_target = PermissionTargetModel.query.filter_by( permission_target = AuthorizationService.find_or_create_permission_target(
uri=target_uri target_uri
).first() )
if permission_target is None:
permission_target = PermissionTargetModel(uri=target_uri)
db.session.add(permission_target)
db.session.commit()
if permission_names is None: if permission_names is None:
permission_names = [member.name for member in Permission] permission_names = [member.name for member in Permission]

View File

@ -1,4 +1,7 @@
"""Test_authentication.""" """Test_authentication."""
import base64
import jwt
from flask import Flask from flask import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest
@ -44,13 +47,16 @@ class TestFlaskOpenId(BaseTest):
client: FlaskClient, client: FlaskClient,
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
) -> None: ) -> None:
"""Test_get_token."""
code = "testadmin1:1234123412341234"
"""It should be possible to get a token.""" """It should be possible to get a token."""
code = ( backend_basic_auth_string = code
"c3BpZmZ3b3JrZmxvdy1iYWNrZW5kOkpYZVFFeG0wSmhRUEx1bWdIdElJcWY1MmJEYWxIejBx" backend_basic_auth_bytes = bytes(backend_basic_auth_string, encoding="ascii")
) backend_basic_auth = base64.b64encode(backend_basic_auth_bytes)
headers = { headers = {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Basic {code}", "Authorization": f"Basic {backend_basic_auth.decode('utf-8')}",
} }
data = { data = {
"grant_type": "authorization_code", "grant_type": "authorization_code",
@ -59,3 +65,13 @@ class TestFlaskOpenId(BaseTest):
} }
response = client.post("/openid/token", data=data, headers=headers) response = client.post("/openid/token", data=data, headers=headers)
assert response assert response
assert response.is_json
assert "access_token" in response.json
assert "id_token" in response.json
assert "refresh_token" in response.json
decoded_token = jwt.decode(
response.json["id_token"], options={"verify_signature": False}
)
assert "iss" in decoded_token
assert "email" in decoded_token

View File

@ -0,0 +1,60 @@
"""Test_get_localtime."""
from flask.app import Flask
from flask.testing import FlaskClient
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.scripts.get_all_permissions import GetAllPermissions
from spiffworkflow_backend.services.authorization_service import AuthorizationService
class TestGetAllPermissions(BaseTest):
"""TestGetAllPermissions."""
def test_can_get_all_permissions(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
"""Test_can_get_all_permissions."""
self.find_or_create_user("test_user")
# now that we have everything, try to clear it out...
script_attributes_context = ScriptAttributesContext(
task=None,
environment_identifier="testing",
process_instance_id=1,
process_model_identifier="my_test_user",
)
AuthorizationService.add_permission_from_uri_or_macro(
permission="start", target="PG:hey:group", group_identifier="my_test_group"
)
AuthorizationService.add_permission_from_uri_or_macro(
permission="all", target="/tasks", group_identifier="my_test_group"
)
expected_permissions = [
{
"group_identifier": "my_test_group",
"uri": "/process-instances/hey:group:%",
"permissions": ["create"],
},
{
"group_identifier": "my_test_group",
"uri": "/process-instances/for-me/hey:group:%",
"permissions": ["read"],
},
{
"group_identifier": "my_test_group",
"uri": "/tasks",
"permissions": ["create", "delete", "read", "update"],
},
]
permissions = GetAllPermissions().run(script_attributes_context)
assert permissions == expected_permissions

View File

@ -24,7 +24,6 @@ class TestSaveProcessInstanceMetadata(BaseTest):
with_super_admin_user: UserModel, with_super_admin_user: UserModel,
) -> None: ) -> None:
"""Test_can_save_process_instance_metadata.""" """Test_can_save_process_instance_metadata."""
initiator_user = self.find_or_create_user("initiator_user")
self.create_process_group( self.create_process_group(
client, with_super_admin_user, "test_group", "test_group" client, with_super_admin_user, "test_group", "test_group"
) )
@ -34,7 +33,7 @@ class TestSaveProcessInstanceMetadata(BaseTest):
process_model_source_directory="save_process_instance_metadata", process_model_source_directory="save_process_instance_metadata",
) )
process_instance = self.create_process_instance_from_process_model( process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=initiator_user process_model=process_model, user=with_super_admin_user
) )
processor = ProcessInstanceProcessor(process_instance) processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True) processor.do_engine_steps(save=True)

View File

@ -4,9 +4,12 @@ from flask import Flask
from flask.testing import FlaskClient from flask.testing import FlaskClient
from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.models.user import UserNotFoundError from spiffworkflow_backend.models.user import UserNotFoundError
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.authorization_service import InvalidPermissionError
from spiffworkflow_backend.services.group_service import GroupService
from spiffworkflow_backend.services.process_instance_processor import ( from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor, ProcessInstanceProcessor,
) )
@ -14,6 +17,7 @@ from spiffworkflow_backend.services.process_instance_service import (
ProcessInstanceService, ProcessInstanceService,
) )
from spiffworkflow_backend.services.process_model_service import ProcessModelService from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.user_service import UserService
class TestAuthorizationService(BaseTest): class TestAuthorizationService(BaseTest):
@ -134,8 +138,339 @@ class TestAuthorizationService(BaseTest):
human_task.task_name, processor.bpmn_process_instance human_task.task_name, processor.bpmn_process_instance
) )
finance_user = AuthorizationService.create_user_from_sign_in( finance_user = AuthorizationService.create_user_from_sign_in(
{"username": "testuser2", "sub": "open_id"} {
"username": "testuser2",
"sub": "testuser2",
"iss": "https://test.stuff",
"email": "testuser2",
}
) )
ProcessInstanceService.complete_form_task( ProcessInstanceService.complete_form_task(
processor, spiff_task, {}, finance_user, human_task processor, spiff_task, {}, finance_user, human_task
) )
def test_explode_permissions_all_on_process_group(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_explode_permissions_all_on_process_group."""
expected_permissions = [
("/logs/some-process-group:some-process-model:*", "create"),
("/logs/some-process-group:some-process-model:*", "delete"),
("/logs/some-process-group:some-process-model:*", "read"),
("/logs/some-process-group:some-process-model:*", "update"),
("/process-groups/some-process-group:some-process-model:*", "create"),
("/process-groups/some-process-group:some-process-model:*", "delete"),
("/process-groups/some-process-group:some-process-model:*", "read"),
("/process-groups/some-process-group:some-process-model:*", "update"),
(
"/process-instance-suspend/some-process-group:some-process-model:*",
"create",
),
(
"/process-instance-suspend/some-process-group:some-process-model:*",
"delete",
),
(
"/process-instance-suspend/some-process-group:some-process-model:*",
"read",
),
(
"/process-instance-suspend/some-process-group:some-process-model:*",
"update",
),
(
"/process-instance-terminate/some-process-group:some-process-model:*",
"create",
),
(
"/process-instance-terminate/some-process-group:some-process-model:*",
"delete",
),
(
"/process-instance-terminate/some-process-group:some-process-model:*",
"read",
),
(
"/process-instance-terminate/some-process-group:some-process-model:*",
"update",
),
("/process-instances/some-process-group:some-process-model:*", "create"),
("/process-instances/some-process-group:some-process-model:*", "delete"),
("/process-instances/some-process-group:some-process-model:*", "read"),
("/process-instances/some-process-group:some-process-model:*", "update"),
("/process-models/some-process-group:some-process-model:*", "create"),
("/process-models/some-process-group:some-process-model:*", "delete"),
("/process-models/some-process-group:some-process-model:*", "read"),
("/process-models/some-process-group:some-process-model:*", "update"),
("/task-data/some-process-group:some-process-model:*", "create"),
("/task-data/some-process-group:some-process-model:*", "delete"),
("/task-data/some-process-group:some-process-model:*", "read"),
("/task-data/some-process-group:some-process-model:*", "update"),
]
permissions_to_assign = AuthorizationService.explode_permissions(
"all", "PG:/some-process-group/some-process-model"
)
permissions_to_assign_tuples = sorted(
[(p.target_uri, p.permission) for p in permissions_to_assign]
)
assert permissions_to_assign_tuples == expected_permissions
def test_explode_permissions_start_on_process_group(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_explode_permissions_start_on_process_group."""
expected_permissions = [
(
"/process-instances/for-me/some-process-group:some-process-model:*",
"read",
),
("/process-instances/some-process-group:some-process-model:*", "create"),
]
permissions_to_assign = AuthorizationService.explode_permissions(
"start", "PG:/some-process-group/some-process-model"
)
permissions_to_assign_tuples = sorted(
[(p.target_uri, p.permission) for p in permissions_to_assign]
)
assert permissions_to_assign_tuples == expected_permissions
def test_explode_permissions_all_on_process_model(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_explode_permissions_all_on_process_model."""
expected_permissions = [
("/logs/some-process-group:some-process-model/*", "create"),
("/logs/some-process-group:some-process-model/*", "delete"),
("/logs/some-process-group:some-process-model/*", "read"),
("/logs/some-process-group:some-process-model/*", "update"),
(
"/process-instance-suspend/some-process-group:some-process-model/*",
"create",
),
(
"/process-instance-suspend/some-process-group:some-process-model/*",
"delete",
),
(
"/process-instance-suspend/some-process-group:some-process-model/*",
"read",
),
(
"/process-instance-suspend/some-process-group:some-process-model/*",
"update",
),
(
"/process-instance-terminate/some-process-group:some-process-model/*",
"create",
),
(
"/process-instance-terminate/some-process-group:some-process-model/*",
"delete",
),
(
"/process-instance-terminate/some-process-group:some-process-model/*",
"read",
),
(
"/process-instance-terminate/some-process-group:some-process-model/*",
"update",
),
("/process-instances/some-process-group:some-process-model/*", "create"),
("/process-instances/some-process-group:some-process-model/*", "delete"),
("/process-instances/some-process-group:some-process-model/*", "read"),
("/process-instances/some-process-group:some-process-model/*", "update"),
("/process-models/some-process-group:some-process-model/*", "create"),
("/process-models/some-process-group:some-process-model/*", "delete"),
("/process-models/some-process-group:some-process-model/*", "read"),
("/process-models/some-process-group:some-process-model/*", "update"),
("/task-data/some-process-group:some-process-model/*", "create"),
("/task-data/some-process-group:some-process-model/*", "delete"),
("/task-data/some-process-group:some-process-model/*", "read"),
("/task-data/some-process-group:some-process-model/*", "update"),
]
permissions_to_assign = AuthorizationService.explode_permissions(
"all", "PM:/some-process-group/some-process-model"
)
permissions_to_assign_tuples = sorted(
[(p.target_uri, p.permission) for p in permissions_to_assign]
)
assert permissions_to_assign_tuples == expected_permissions
def test_explode_permissions_start_on_process_model(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_explode_permissions_start_on_process_model."""
expected_permissions = [
(
"/process-instances/for-me/some-process-group:some-process-model/*",
"read",
),
("/process-instances/some-process-group:some-process-model/*", "create"),
]
permissions_to_assign = AuthorizationService.explode_permissions(
"start", "PM:/some-process-group/some-process-model"
)
permissions_to_assign_tuples = sorted(
[(p.target_uri, p.permission) for p in permissions_to_assign]
)
assert permissions_to_assign_tuples == expected_permissions
def test_explode_permissions_basic(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_explode_permissions_basic."""
expected_permissions = [
("/process-instances/for-me", "read"),
("/process-instances/reports/*", "create"),
("/process-instances/reports/*", "delete"),
("/process-instances/reports/*", "read"),
("/process-instances/reports/*", "update"),
("/processes", "read"),
("/service-tasks", "read"),
("/tasks/*", "create"),
("/tasks/*", "delete"),
("/tasks/*", "read"),
("/tasks/*", "update"),
("/user-groups/for-current-user", "read"),
]
permissions_to_assign = AuthorizationService.explode_permissions("all", "BASIC")
permissions_to_assign_tuples = sorted(
[(p.target_uri, p.permission) for p in permissions_to_assign]
)
assert permissions_to_assign_tuples == expected_permissions
def test_explode_permissions_all(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_explode_permissions_all."""
expected_permissions = [
("/*", "create"),
("/*", "delete"),
("/*", "read"),
("/*", "update"),
]
permissions_to_assign = AuthorizationService.explode_permissions("all", "ALL")
permissions_to_assign_tuples = sorted(
[(p.target_uri, p.permission) for p in permissions_to_assign]
)
assert permissions_to_assign_tuples == expected_permissions
def test_explode_permissions_with_target_uri(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_explode_permissions_with_target_uri."""
expected_permissions = [
("/hey/model", "create"),
("/hey/model", "delete"),
("/hey/model", "read"),
("/hey/model", "update"),
]
permissions_to_assign = AuthorizationService.explode_permissions(
"all", "/hey/model"
)
permissions_to_assign_tuples = sorted(
[(p.target_uri, p.permission) for p in permissions_to_assign]
)
assert permissions_to_assign_tuples == expected_permissions
def test_granting_access_to_group_gives_access_to_group_and_subgroups(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_granting_access_to_group_gives_access_to_group_and_subgroups."""
user = self.find_or_create_user(username="user_one")
user_group = GroupService.find_or_create_group("group_one")
UserService.add_user_to_group(user, user_group)
AuthorizationService.add_permission_from_uri_or_macro(
user_group.identifier, "read", "PG:hey"
)
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey")
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo")
def test_explode_permissions_with_invalid_target_uri(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_explode_permissions_with_invalid_target_uri."""
with pytest.raises(InvalidPermissionError):
AuthorizationService.explode_permissions("all", "BAD_MACRO")
def test_explode_permissions_with_start_to_incorrect_target(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_explode_permissions_with_start_to_incorrect_target."""
with pytest.raises(InvalidPermissionError):
AuthorizationService.explode_permissions("start", "/hey/model")
def test_can_refresh_permissions(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_can_refresh_permissions."""
user = self.find_or_create_user(username="user_one")
admin_user = self.find_or_create_user(username="testadmin1")
# this group is not mentioned so it will get deleted
GroupService.find_or_create_group("group_two")
assert GroupModel.query.filter_by(identifier="group_two").first() is not None
group_info = [
{
"users": ["user_one"],
"name": "group_one",
"permissions": [{"actions": ["create", "read"], "uri": "PG:hey"}],
}
]
AuthorizationService.refresh_permissions(group_info)
assert GroupModel.query.filter_by(identifier="group_two").first() is None
assert GroupModel.query.filter_by(identifier="group_one").first() is not None
self.assert_user_has_permission(admin_user, "create", "/anything-they-want")
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey")
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo")
self.assert_user_has_permission(user, "create", "/v1.0/process-groups/hey:yo")
group_info = [
{
"users": ["user_one"],
"name": "group_one",
"permissions": [{"actions": ["read"], "uri": "PG:hey"}],
}
]
AuthorizationService.refresh_permissions(group_info)
assert GroupModel.query.filter_by(identifier="group_one").first() is not None
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey")
self.assert_user_has_permission(user, "read", "/v1.0/process-groups/hey:yo")
self.assert_user_has_permission(
user, "create", "/v1.0/process-groups/hey:yo", expected_result=False
)
self.assert_user_has_permission(admin_user, "create", "/anything-they-want")

View File

@ -0,0 +1,54 @@
"""Process Model."""
from flask.app import Flask
from flask.testing import FlaskClient
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from spiffworkflow_backend.models.user_group_assignment_waiting import (
UserGroupAssignmentWaitingModel,
)
from spiffworkflow_backend.services.group_service import GroupService
from spiffworkflow_backend.services.user_service import UserService
class TestUserService(BaseTest):
"""TestUserService."""
def test_assigning_a_group_to_a_user_before_the_user_is_created(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_waiting_group_assignments."""
a_test_group = GroupService.find_or_create_group("aTestGroup")
UserService.add_waiting_group_assignment("initiator_user", a_test_group)
initiator_user = self.find_or_create_user("initiator_user")
assert initiator_user.groups[0] == a_test_group
def test_assigning_a_group_to_all_users_updates_new_users(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_waiting_group_assignments."""
everybody_group = GroupService.find_or_create_group("everybodyGroup")
UserService.add_waiting_group_assignment(
UserGroupAssignmentWaitingModel.MATCH_ALL_USERS, everybody_group
)
initiator_user = self.find_or_create_user("initiator_user")
assert initiator_user.groups[0] == everybody_group
def test_assigning_a_group_to_all_users_updates_existing_users(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_waiting_group_assignments."""
initiator_user = self.find_or_create_user("initiator_user")
everybody_group = GroupService.find_or_create_group("everybodyGroup")
UserService.add_waiting_group_assignment(
UserGroupAssignmentWaitingModel.MATCH_ALL_USERS, everybody_group
)
assert initiator_user.groups[0] == everybody_group

View File

@ -56,6 +56,8 @@ export default function ProcessModelEditDiagram() {
const [processSearchEventBus, setProcessSearchEventBus] = useState<any>(null); const [processSearchEventBus, setProcessSearchEventBus] = useState<any>(null);
const [processSearchElement, setProcessSearchElement] = useState<any>(null); const [processSearchElement, setProcessSearchElement] = useState<any>(null);
const [processes, setProcesses] = useState<ProcessReference[]>([]); const [processes, setProcesses] = useState<ProcessReference[]>([]);
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
useState<boolean>(false);
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true); const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
@ -79,10 +81,10 @@ export default function ProcessModelEditDiagram() {
interface ScriptUnitTestResult { interface ScriptUnitTestResult {
result: boolean; result: boolean;
context: object; context?: object;
error: string; error?: string;
line_number: number; line_number?: number;
offset: number; offset?: number;
} }
const [currentScriptUnitTest, setCurrentScriptUnitTest] = const [currentScriptUnitTest, setCurrentScriptUnitTest] =
@ -157,6 +159,7 @@ export default function ProcessModelEditDiagram() {
}; };
const navigateToProcessModelFile = (_result: any) => { const navigateToProcessModelFile = (_result: any) => {
setDisplaySaveFileMessage(true);
if (!params.file_name) { if (!params.file_name) {
const fileNameWithExtension = `${newFileName}.${searchParams.get( const fileNameWithExtension = `${newFileName}.${searchParams.get(
'file_type' 'file_type'
@ -167,9 +170,8 @@ export default function ProcessModelEditDiagram() {
} }
}; };
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
useState<boolean>(false);
const saveDiagram = (bpmnXML: any, fileName = params.file_name) => { const saveDiagram = (bpmnXML: any, fileName = params.file_name) => {
setDisplaySaveFileMessage(false);
setErrorMessage(null); setErrorMessage(null);
setBpmnXmlForDiagramRendering(bpmnXML); setBpmnXmlForDiagramRendering(bpmnXML);
@ -204,7 +206,6 @@ export default function ProcessModelEditDiagram() {
// after saving the file, make sure we null out newFileName // after saving the file, make sure we null out newFileName
// so it does not get used over the params // so it does not get used over the params
setNewFileName(''); setNewFileName('');
setDisplaySaveFileMessage(true);
}; };
const onDeleteFile = (fileName = params.file_name) => { const onDeleteFile = (fileName = params.file_name) => {
@ -477,6 +478,21 @@ export default function ProcessModelEditDiagram() {
const runCurrentUnitTest = () => { const runCurrentUnitTest = () => {
if (currentScriptUnitTest && scriptElement) { if (currentScriptUnitTest && scriptElement) {
let inputJson = '';
let expectedJson = '';
try {
inputJson = JSON.parse(currentScriptUnitTest.inputJson.value);
expectedJson = JSON.parse(
currentScriptUnitTest.expectedOutputJson.value
);
} catch (e) {
setScriptUnitTestResult({
result: false,
error: 'The JSON provided contains a formatting error.',
});
return;
}
resetUnitTextResult(); resetUnitTextResult();
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/script-unit-tests/run`, path: `/process-models/${modifiedProcessModelId}/script-unit-tests/run`,
@ -485,31 +501,29 @@ export default function ProcessModelEditDiagram() {
postBody: { postBody: {
bpmn_task_identifier: (scriptElement as any).id, bpmn_task_identifier: (scriptElement as any).id,
python_script: scriptText, python_script: scriptText,
input_json: JSON.parse(currentScriptUnitTest.inputJson.value), input_json: inputJson,
expected_output_json: JSON.parse( expected_output_json: expectedJson,
currentScriptUnitTest.expectedOutputJson.value
),
}, },
}); });
} }
}; };
const unitTestFailureElement = () => { const unitTestFailureElement = () => {
if ( if (scriptUnitTestResult && scriptUnitTestResult.result === false) {
scriptUnitTestResult && let errorMessage = '';
scriptUnitTestResult.result === false && if (scriptUnitTestResult.context) {
!scriptUnitTestResult.line_number errorMessage = 'Unexpected result. Please see the comparison below.';
) { } else if (scriptUnitTestResult.line_number) {
let errorStringElement = null; errorMessage = `Error encountered running the script. Please check the code around line ${scriptUnitTestResult.line_number}`;
if (scriptUnitTestResult.error) { } else {
errorStringElement = ( errorMessage = `Error encountered running the script. ${JSON.stringify(
<span> scriptUnitTestResult.error
Received error when running script:{' '} )}`;
{JSON.stringify(scriptUnitTestResult.error)}
</span>
);
} }
let errorStringElement = <span>{errorMessage}</span>;
let errorContextElement = null; let errorContextElement = null;
if (scriptUnitTestResult.context) { if (scriptUnitTestResult.context) {
errorStringElement = ( errorStringElement = (
<span>Unexpected result. Please see the comparison below.</span> <span>Unexpected result. Please see the comparison below.</span>
@ -580,16 +594,22 @@ export default function ProcessModelEditDiagram() {
</Col> </Col>
); );
} }
const inputJson = JSON.stringify( let inputJson = currentScriptUnitTest.inputJson.value;
JSON.parse(currentScriptUnitTest.inputJson.value), let outputJson = currentScriptUnitTest.expectedOutputJson.value;
null, try {
' ' inputJson = JSON.stringify(
); JSON.parse(currentScriptUnitTest.inputJson.value),
const outputJson = JSON.stringify( null,
JSON.parse(currentScriptUnitTest.expectedOutputJson.value), ' '
null, );
' ' outputJson = JSON.stringify(
); JSON.parse(currentScriptUnitTest.expectedOutputJson.value),
null,
' '
);
} catch (e) {
// Attemping to format the json failed -- it's invalid.
}
return ( return (
<main> <main>