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."""
app = get_hacked_up_app_for_script()
with app.app_context():
AuthorizationService.delete_all_permissions_and_recreate()
AuthorizationService.delete_all_permissions()
AuthorizationService.import_permissions_from_yaml_file()
if __name__ == "__main__":

View File

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

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: b99a4cb94b5b
Revision ID: 67197b02b0c1
Revises:
Create Date: 2022-12-20 10:45:08.295317
Create Date: 2022-12-20 15:05:31.545567
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b99a4cb94b5b'
revision = '67197b02b0c1'
down_revision = None
branch_labels = None
depends_on = None
@ -72,16 +72,15 @@ def upgrade():
op.create_table('user',
sa.Column('id', sa.Integer(), 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_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('updated_at_in_seconds', sa.Integer(), nullable=True),
sa.Column('created_at_in_seconds', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('service', 'service_id', name='service_key'),
sa.UniqueConstraint('uid')
sa.UniqueConstraint('username')
)
op.create_table('message_correlation_property',
sa.Column('id', sa.Integer(), nullable=False),
@ -176,6 +175,14 @@ def upgrade():
sa.PrimaryKeyConstraint('id'),
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',
sa.Column('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_index(op.f('ix_human_task_completed'), table_name='human_task')
op.drop_table('human_task')
op.drop_table('user_group_assignment_waiting')
op.drop_table('user_group_assignment')
op.drop_table('secret')
op.drop_table('refresh_token')

View File

@ -18,6 +18,7 @@ from werkzeug.exceptions import NotFound
import spiffworkflow_backend.load_database_models # noqa: F401
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.openid_blueprint.openid_blueprint import (
openid_blueprint,
@ -117,7 +118,7 @@ def create_app() -> flask.app.Flask:
]
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)
app.config["MAIL_APP"] = mail

View File

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

View File

@ -2,14 +2,17 @@ default_group: everybody
users:
admin:
service: local_open_id
email: admin@spiffworkflow.org
password: admin
preferred_username: Admin
nelson:
service: local_open_id
email: nelson@spiffworkflow.org
password: nelson
preferred_username: Nelson
malala:
service: local_open_id
email: malala@spiffworkflow.org
password: malala
preferred_username: Malala
@ -18,17 +21,17 @@ groups:
admin:
users:
[
admin,
admin@spiffworkflow.org,
]
Education:
users:
[
malala
malala@spiffworkflow.org
]
President:
users:
[
nelson
nelson@spiffworkflow.org
]
permissions:
@ -44,45 +47,44 @@ permissions:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
uri: /tasks/*
# Everyone can see everything (all groups, and processes are visible)
read-all-process-groups:
groups: [ everybody ]
users: [ ]
allowed_permissions: [ read ]
uri: /v1.0/process-groups/*
uri: /process-groups/*
read-all-process-models:
groups: [ everybody ]
users: [ ]
allowed_permissions: [ read ]
uri: /v1.0/process-models/*
uri: /process-models/*
read-all-process-instance:
groups: [ everybody ]
users: [ ]
allowed_permissions: [ read ]
uri: /v1.0/process-instances/*
uri: /process-instances/*
read-process-instance-reports:
groups: [ everybody ]
users: [ ]
allowed_permissions: [ read ]
uri: /v1.0/process-instances/reports/*
uri: /process-instances/reports/*
processes-read:
groups: [ everybody ]
users: [ ]
allowed_permissions: [ read ]
uri: /v1.0/processes
# Members of the Education group can change they processes work.
uri: /processes
# Members of the Education group can change the processes under "education".
education-admin:
groups: ["Education", "President"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/education:*
uri: /process-groups/education:*
# Anyone can start an education process.
education-everybody:
groups: [everybody]
users: []
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:
users:
[
admin,
jakub,
kb,
alex,
dan,
mike,
jason,
j,
jarrad,
elizabeth,
jon,
natalia,
admin@status.im,
jakub@status.im,
jarrad@status.im,
kb@sartography.com,
alex@sartography.com,
dan@sartography.com,
mike@sartography.com,
jason@sartography.com,
j@sartography.com,
elizabeth@sartography.com,
jon@sartography.com,
]
Finance Team:
users:
[
jakub,
alex,
dan,
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
natalia,
sasha,
fin,
fin1,
jakub@status.im,
amir@status.im,
jarrad@status.im,
sasha@status.im,
fin@sartography.com,
fin1@sartography.com,
alex@sartography.com,
dan@sartography.com,
mike@sartography.com,
jason@sartography.com,
j@sartography.com,
elizabeth@sartography.com,
jon@sartography.com,
]
demo:
users:
[
core,
fin,
fin1,
harmeet,
sasha,
manuchehr,
lead,
lead1
]
core-contributor:
users:
[
core,
harmeet,
harmeet@status.im,
sasha@status.im,
manuchehr@status.im,
core@status.im,
fin@status.im,
fin1@status.im,
lead@status.im,
lead1@status.im
]
permissions:
@ -67,104 +58,86 @@ permissions:
groups: [admin]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/*
uri: /process-instances/*
tasks-crud:
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
# open system defaults for everybody
read-all-process-groups:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-groups/*
uri: /process-groups/*
read-all-process-models:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-models/*
uri: /process-models/*
# basic perms for everybody
read-all-process-instances-for-me:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances/for-me/*
manage-process-instance-reports:
uri: /process-instances/for-me/*
read-process-instance-reports:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/reports/*
uri: /process-instances/reports/*
processes-read:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/processes
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"]
uri: /processes
service-tasks:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/logs/manage-procurement:*
manage-procurement-admin-instance-logs-slash:
groups: ["Project Lead"]
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: /v1.0/logs/manage-procurement/*
uri: /user-groups/for-current-user
manage-revenue-streams-instances:
groups: ["core-contributor", "demo"]
groups: ["demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/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/*
allowed_permissions: [create]
uri: /process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"]
groups: ["demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/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:*
allowed_permissions: [create]
uri: /process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances:
groups: ["core-contributor", "demo"]
groups: ["demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:*
manage-procurement-instance-logs:
groups: ["core-contributor", "demo"]
allowed_permissions: [create]
uri: /process-instances/manage-procurement:vendor-lifecycle-management:*
manage-revenue-streams-instances-for-me:
groups: ["demo"]
users: []
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:
users:
[
admin,
jakub,
kb,
alex,
dan,
mike,
jason,
j,
jarrad,
elizabeth,
jon,
admin@status.im,
jakub@status.im,
jarrad@status.im,
kb@sartography.com,
alex@sartography.com,
dan@sartography.com,
mike@sartography.com,
jason@sartography.com,
j@sartography.com,
elizabeth@sartography.com,
jon@sartography.com,
]
Finance Team:
users:
[
jakub,
alex,
dan,
mike,
jason,
j,
amir,
jarrad,
elizabeth,
jon,
sasha,
fin,
fin1,
jakub@status.im,
amir@status.im,
jarrad@status.im,
sasha@status.im,
fin@sartography.com,
fin1@sartography.com,
alex@sartography.com,
dan@sartography.com,
mike@sartography.com,
jason@sartography.com,
j@sartography.com,
elizabeth@sartography.com,
jon@sartography.com,
]
demo:
users:
[
core,
fin,
fin1,
harmeet,
sasha,
manuchehr,
lead,
lead1
harmeet@status.im,
sasha@status.im,
manuchehr@status.im,
core@status.im,
fin@status.im,
fin1@status.im,
lead@status.im,
lead1@status.im
]
core-contributor:
users:
[
core,
harmeet,
]
test:
users: [natalia]
users: [natalia@sartography.com]
permissions:
admin:
@ -64,109 +58,91 @@ permissions:
allowed_permissions: [create, read, update, delete]
uri: /*
tasks-crud:
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
# open system defaults for everybody
read-all-process-groups:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-groups/*
uri: /process-groups/*
read-all-process-models:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-models/*
uri: /process-models/*
# basic perms for everybody
read-all-process-instances-for-me:
groups: [everybody]
users: []
allowed_permissions: [read]
uri: /v1.0/process-instances/for-me/*
uri: /process-instances/for-me/*
read-process-instance-reports:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-instances/reports/*
uri: /process-instances/reports/*
processes-read:
groups: [everybody]
users: []
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:
groups: ["Finance Team"]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/manage-procurement:procurement:*
uri: /process-groups/manage-procurement:procurement:*
manage-revenue-streams-instances:
groups: ["core-contributor", "demo"]
groups: ["demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
allowed_permissions: [create]
uri: /process-instances/manage-revenue-streams:product-revenue-streams:customer-contracts-trade-terms/*
manage-procurement-invoice-instances:
groups: ["core-contributor", "demo"]
groups: ["demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
allowed_permissions: [create]
uri: /process-instances/manage-procurement:procurement:core-contributor-invoice-management:*
manage-procurement-instances:
groups: ["core-contributor", "demo"]
groups: ["demo"]
users: []
allowed_permissions: [create, read]
uri: /v1.0/process-instances/manage-procurement:vendor-lifecycle-management:*
allowed_permissions: [create]
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:
groups: ["test"]
users: []
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
users:
testadmin1:
service: https://testing/openid/thing
email: testadmin1@spiffworkflow.org
password: admin
preferred_username: El administrador de la muerte
groups:
admin:
users: [testadmin1, testadmin2]
@ -14,7 +21,7 @@ permissions:
admin:
groups: [admin]
users: []
allowed_permissions: [create, read, update, delete, list, instantiate]
allowed_permissions: [create, read, update, delete]
uri: /*
read-all:
@ -27,29 +34,29 @@ permissions:
groups: [everybody]
users: []
allowed_permissions: [create, read, update, delete]
uri: /v1.0/tasks/*
uri: /tasks/*
# TODO: all uris should really have the same structure
finance-admin-group:
groups: ["Finance Team"]
users: [testuser4]
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-groups/finance/*
uri: /process-groups/finance/*
finance-admin-model:
groups: ["Finance Team"]
users: [testuser4]
allowed_permissions: [create, read, update, delete]
uri: /v1.0/process-models/finance/*
uri: /process-models/finance/*
finance-admin-model-lanes:
groups: ["Finance Team"]
users: [testuser4]
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:
groups: ["Finance Team"]
users: [testuser4]
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))
user_group_assignments = relationship("UserGroupAssignmentModel", cascade="delete")
user_group_assignments_waiting = relationship( # type: ignore
"UserGroupAssignmentWaitingModel", cascade="delete"
)
users = relationship( # type: ignore
"UserModel",
viewonly=True,

View File

@ -32,14 +32,6 @@ class Permission(enum.Enum):
update = "update"
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):
"""PermissionAssignmentModel."""

View File

@ -1,22 +1,15 @@
"""User."""
from __future__ import annotations
from typing import Any
import jwt
import marshmallow
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 SpiffworkflowBaseDBModel
from marshmallow import Schema
from sqlalchemy.orm import relationship
from sqlalchemy.orm import validates
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.services.authentication_service import (
AuthenticationProviderTypes,
)
class UserNotFoundError(Exception):
@ -28,14 +21,15 @@ class UserModel(SpiffworkflowBaseDBModel):
__tablename__ = "user"
__table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),)
id = db.Column(db.Integer, primary_key=True)
# server and service id must be unique, not username.
username = db.Column(db.String(255), nullable=False, unique=False)
uid = db.Column(db.String(50), unique=True)
service = db.Column(db.String(50), nullable=False, unique=False)
username = db.Column(
db.String(255), nullable=False, unique=True
) # should always be a unique value
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)
name = db.Column(db.String(255))
display_name = db.Column(db.String(255))
email = db.Column(db.String(255))
updated_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
@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:
"""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"])
def process_model_run(process_model_id: str) -> Union[str, Response]:
"""Process_model_run."""
user = UserService.create_user("internal", "Mr. Test", username="Mr. Test")
user = UserService.create_user("Mr. Test", "internal", "Mr. Test")
process_instance = (
ProcessInstanceService.create_process_instance_from_process_model_identifier(
process_model_id, user

View File

@ -111,6 +111,7 @@ def token() -> dict:
"iat": time.time(),
"exp": time.time() + 86400, # Expire after a day.
"sub": user_name,
"email": user_details["email"],
"preferred_username": user_details.get("preferred_username", user_name),
},
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.
# Try to refresh the token
user = UserService.get_user_by_service_and_service_id(
"open_id", decoded_token["sub"]
decoded_token["iss"], decoded_token["sub"]
)
if user:
refresh_token = AuthenticationService.get_refresh_token(user.id)
@ -105,10 +105,12 @@ def verify_token(
) from e
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
user_model = (
UserModel.query.filter(UserModel.service == "open_id")
UserModel.query.filter(UserModel.service == user_info["iss"])
.filter(UserModel.service_id == user_info["sub"])
.first()
)
@ -341,9 +343,5 @@ def get_user_from_decoded_internal_token(decoded_token: dict) -> Optional[UserMo
)
if user:
return user
user = UserModel(
username=service_id,
service=service,
service_id=service_id,
)
user = UserService.create_user(service_id, service, service_id)
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):
"""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:
"""Get_description."""
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):
"""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:
"""Get_description."""
return """Return the current user."""

View File

@ -10,6 +10,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetEnv(Script):
"""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:
"""Get_description."""
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):
"""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:
"""Get_description."""
return """Return the url to the frontend."""

View File

@ -12,6 +12,11 @@ from spiffworkflow_backend.scripts.script import Script
class GetGroupMembers(Script):
"""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:
"""Get_description."""
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):
"""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:
"""Get_description."""
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):
"""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:
"""Get_description."""
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 spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceNotFoundError
from spiffworkflow_backend.models.script_attributes_context import (
ScriptAttributesContext,
)
from spiffworkflow_backend.services.authorization_service import AuthorizationService
# Generally speaking, having some global in a flask app is TERRIBLE.
# 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
class ScriptUnauthorizedForUserError(Exception):
"""ScriptUnauthorizedForUserError."""
class Script:
"""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.",
)
@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
def generate_augmented_list(
script_attributes_context: ScriptAttributesContext,
@ -71,18 +87,50 @@ class Script:
that we created.
"""
instance = subclass()
return lambda *ar, **kw: subclass.run(
instance,
script_attributes_context,
*ar,
**kw,
)
def check_script_permission() -> None:
"""Check_script_permission."""
if subclass.requires_privileged_permissions():
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 = {}
subclasses = Script.get_all_subclasses()
for x in range(len(subclasses)):
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
)
return execlist

View File

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

View File

@ -1,10 +1,14 @@
"""Authorization_service."""
import inspect
import re
from dataclasses import dataclass
from hashlib import sha256
from hmac import compare_digest
from hmac import HMAC
from typing import Any
from typing import Optional
from typing import Set
from typing import TypedDict
from typing import Union
import jwt
@ -19,6 +23,7 @@ from SpiffWorkflow.task import Task as SpiffTask # type: ignore
from sqlalchemy import or_
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.human_task import HumanTaskModel
from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel
@ -45,6 +50,34 @@ class UserDoesNotHaveAccessToTaskError(Exception):
"""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:
"""Determine whether a user has permission to perform their request."""
@ -75,6 +108,7 @@ class AuthorizationService:
) -> bool:
"""Has_permission."""
principal_ids = [p.id for p in principals]
target_uri_normalized = target_uri.removeprefix(V1_API_PATH_PREFIX)
permission_assignments = (
PermissionAssignmentModel.query.filter(
@ -84,10 +118,12 @@ class AuthorizationService:
.join(PermissionTargetModel)
.filter(
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
# 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()
@ -127,17 +163,15 @@ class AuthorizationService:
return cls.has_permission(principals, permission, target_uri)
@classmethod
def delete_all_permissions_and_recreate(cls) -> None:
"""Delete_all_permissions_and_recreate."""
def delete_all_permissions(cls) -> None:
"""Delete_all_permissions_and_recreate. EXCEPT For permissions for the current user?"""
for model in [PermissionAssignmentModel, PermissionTargetModel]:
db.session.query(model).delete()
# 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():
db.session.delete(group)
db.session.commit()
cls.import_permissions_from_yaml_file()
@classmethod
def associate_user_with_group(cls, user: UserModel, group: GroupModel) -> None:
@ -155,7 +189,7 @@ class AuthorizationService:
@classmethod
def import_permissions_from_yaml_file(
cls, raise_if_missing_user: bool = False
) -> None:
) -> DesiredPermissionDict:
"""Import_permissions_from_yaml_file."""
if current_app.config["SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME"] is None:
raise (
@ -169,13 +203,16 @@ class AuthorizationService:
permission_configs = yaml.safe_load(file)
default_group = None
unique_user_group_identifiers: Set[str] = set()
if "default_group" in permission_configs:
default_group_identifier = permission_configs["default_group"]
default_group = GroupService.find_or_create_group(default_group_identifier)
unique_user_group_identifiers.add(default_group_identifier)
if "groups" in permission_configs:
for group_identifier, group_config in permission_configs["groups"].items():
group = GroupService.find_or_create_group(group_identifier)
unique_user_group_identifiers.add(group_identifier)
for username in group_config["users"]:
user = UserModel.query.filter_by(username=username).first()
if user is None:
@ -188,26 +225,25 @@ class AuthorizationService:
continue
cls.associate_user_with_group(user, group)
permission_assignments = []
if "permissions" in permission_configs:
for _permission_identifier, permission_config in permission_configs[
"permissions"
].items():
uri = permission_config["uri"]
uri_with_percent = re.sub(r"\*", "%", 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()
permission_target = cls.find_or_create_permission_target(uri)
for allowed_permission in permission_config["allowed_permissions"]:
if "groups" in permission_config:
for group_identifier in permission_config["groups"]:
group = GroupService.find_or_create_group(group_identifier)
cls.create_permission_for_principal(
group.principal, permission_target, allowed_permission
unique_user_group_identifiers.add(group_identifier)
permission_assignments.append(
cls.create_permission_for_principal(
group.principal,
permission_target,
allowed_permission,
)
)
if "users" in permission_config:
for username in permission_config["users"]:
@ -218,14 +254,35 @@ class AuthorizationService:
.filter(UserModel.username == username)
.first()
)
cls.create_permission_for_principal(
principal, permission_target, allowed_permission
permission_assignments.append(
cls.create_permission_for_principal(
principal, permission_target, allowed_permission
)
)
if default_group is not None:
for user in UserModel.query.all():
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
def create_permission_for_principal(
cls,
@ -449,33 +506,48 @@ class AuthorizationService:
@classmethod
def create_user_from_sign_in(cls, user_info: dict) -> UserModel:
"""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
user_model = (
UserModel.query.filter(UserModel.service == "open_id")
UserModel.query.filter(UserModel.service == user_info["iss"])
.filter(UserModel.service_id == user_info["sub"])
.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:
current_app.logger.debug("create_user in login_return")
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(
service="open_id",
service_id=user_info["sub"],
name=name,
username=username,
service=user_info["iss"],
service_id=user_info["sub"],
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.
# when it does, be careful about backgrounding, because
# the user will immediately need permissions to use the site.
@ -490,6 +562,212 @@ class AuthorizationService:
# this cannot be None so ignore mypy
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:
"""Interface with Keycloak server."""

View File

@ -4,6 +4,7 @@ from typing import Optional
from flask_bpmn.models.db import db
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.user_service import UserService
@ -22,3 +23,15 @@ class GroupService:
db.session.commit()
UserService.create_principal(group.id, id_column_name="group_id")
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,
"decimal": decimal,
"_strptime": _strptime,
"enumerate": enumerate,
}
# 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:
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:
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(
GroupModel,
and_(*group_model_join_conditions)
GroupModel, and_(*group_model_join_conditions)
)
process_instance_query = process_instance_query.join(
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.user import UserModel
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 (
ProcessInstanceProcessor,
)
@ -38,7 +39,7 @@ class ProcessInstanceService:
"""Get_process_instance_from_spec."""
try:
current_git_revision = GitService.get_current_revision()
except GitCommandError as ge:
except GitCommandError:
current_git_revision = ""
process_instance_model = ProcessInstanceModel(
status=ProcessInstanceStatus.not_started.value,

View File

@ -224,10 +224,10 @@ class ProcessModelService(FileSystemService):
new_process_model_list = []
for process_model in process_models:
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
)
if result:
if has_permission:
new_process_model_list.append(process_model)
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.user import UserModel
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
from spiffworkflow_backend.models.user_group_assignment_waiting import (
UserGroupAssignmentWaitingModel,
)
class UserService:
@ -21,11 +24,11 @@ class UserService:
@classmethod
def create_user(
cls,
username: str,
service: str,
service_id: str,
name: Optional[str] = "",
username: Optional[str] = "",
email: Optional[str] = "",
display_name: Optional[str] = "",
) -> UserModel:
"""Create_user."""
user_model: Optional[UserModel] = (
@ -41,8 +44,8 @@ class UserService:
username=username,
service=service,
service_id=service_id,
name=name,
email=email,
display_name=display_name,
)
db.session.add(user_model)
@ -55,6 +58,7 @@ class UserService:
message=f"Could not add user {username}",
) from e
cls.create_principal(user_model.id)
UserService().apply_waiting_group_assignments(user_model)
return user_model
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.
@staticmethod
def has_user() -> bool:
"""Has_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
def current_user() -> Any:
"""Current_user."""
@ -117,20 +88,6 @@ class UserService:
)
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
def get_principal_by_user_id(user_id: int) -> PrincipalModel:
"""Get_principal_by_user_id."""
@ -173,8 +130,57 @@ class UserService:
@classmethod
def add_user_to_group(cls, user: UserModel, group: GroupModel) -> None:
"""Add_user_to_group."""
ugam = UserGroupAssignmentModel(user_id=user.id, group_id=group.id)
db.session.add(ugam)
exists = (
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()
@staticmethod

View File

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

View File

@ -1,4 +1,7 @@
"""Test_authentication."""
import base64
import jwt
from flask import Flask
from flask.testing import FlaskClient
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
@ -44,13 +47,16 @@ class TestFlaskOpenId(BaseTest):
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_get_token."""
code = "testadmin1:1234123412341234"
"""It should be possible to get a token."""
code = (
"c3BpZmZ3b3JrZmxvdy1iYWNrZW5kOkpYZVFFeG0wSmhRUEx1bWdIdElJcWY1MmJEYWxIejBx"
)
backend_basic_auth_string = code
backend_basic_auth_bytes = bytes(backend_basic_auth_string, encoding="ascii")
backend_basic_auth = base64.b64encode(backend_basic_auth_bytes)
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Basic {code}",
"Authorization": f"Basic {backend_basic_auth.decode('utf-8')}",
}
data = {
"grant_type": "authorization_code",
@ -59,3 +65,13 @@ class TestFlaskOpenId(BaseTest):
}
response = client.post("/openid/token", data=data, headers=headers)
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,
) -> None:
"""Test_can_save_process_instance_metadata."""
initiator_user = self.find_or_create_user("initiator_user")
self.create_process_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_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.do_engine_steps(save=True)

View File

@ -4,9 +4,12 @@ from flask import Flask
from flask.testing import FlaskClient
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 UserNotFoundError
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 (
ProcessInstanceProcessor,
)
@ -14,6 +17,7 @@ from spiffworkflow_backend.services.process_instance_service import (
ProcessInstanceService,
)
from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.user_service import UserService
class TestAuthorizationService(BaseTest):
@ -134,8 +138,339 @@ class TestAuthorizationService(BaseTest):
human_task.task_name, processor.bpmn_process_instance
)
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(
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 [processSearchElement, setProcessSearchElement] = useState<any>(null);
const [processes, setProcesses] = useState<ProcessReference[]>([]);
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
useState<boolean>(false);
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
@ -79,10 +81,10 @@ export default function ProcessModelEditDiagram() {
interface ScriptUnitTestResult {
result: boolean;
context: object;
error: string;
line_number: number;
offset: number;
context?: object;
error?: string;
line_number?: number;
offset?: number;
}
const [currentScriptUnitTest, setCurrentScriptUnitTest] =
@ -157,6 +159,7 @@ export default function ProcessModelEditDiagram() {
};
const navigateToProcessModelFile = (_result: any) => {
setDisplaySaveFileMessage(true);
if (!params.file_name) {
const fileNameWithExtension = `${newFileName}.${searchParams.get(
'file_type'
@ -167,9 +170,8 @@ export default function ProcessModelEditDiagram() {
}
};
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
useState<boolean>(false);
const saveDiagram = (bpmnXML: any, fileName = params.file_name) => {
setDisplaySaveFileMessage(false);
setErrorMessage(null);
setBpmnXmlForDiagramRendering(bpmnXML);
@ -204,7 +206,6 @@ export default function ProcessModelEditDiagram() {
// after saving the file, make sure we null out newFileName
// so it does not get used over the params
setNewFileName('');
setDisplaySaveFileMessage(true);
};
const onDeleteFile = (fileName = params.file_name) => {
@ -477,6 +478,21 @@ export default function ProcessModelEditDiagram() {
const runCurrentUnitTest = () => {
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();
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/script-unit-tests/run`,
@ -485,31 +501,29 @@ export default function ProcessModelEditDiagram() {
postBody: {
bpmn_task_identifier: (scriptElement as any).id,
python_script: scriptText,
input_json: JSON.parse(currentScriptUnitTest.inputJson.value),
expected_output_json: JSON.parse(
currentScriptUnitTest.expectedOutputJson.value
),
input_json: inputJson,
expected_output_json: expectedJson,
},
});
}
};
const unitTestFailureElement = () => {
if (
scriptUnitTestResult &&
scriptUnitTestResult.result === false &&
!scriptUnitTestResult.line_number
) {
let errorStringElement = null;
if (scriptUnitTestResult.error) {
errorStringElement = (
<span>
Received error when running script:{' '}
{JSON.stringify(scriptUnitTestResult.error)}
</span>
);
if (scriptUnitTestResult && scriptUnitTestResult.result === false) {
let errorMessage = '';
if (scriptUnitTestResult.context) {
errorMessage = 'Unexpected result. Please see the comparison below.';
} else if (scriptUnitTestResult.line_number) {
errorMessage = `Error encountered running the script. Please check the code around line ${scriptUnitTestResult.line_number}`;
} else {
errorMessage = `Error encountered running the script. ${JSON.stringify(
scriptUnitTestResult.error
)}`;
}
let errorStringElement = <span>{errorMessage}</span>;
let errorContextElement = null;
if (scriptUnitTestResult.context) {
errorStringElement = (
<span>Unexpected result. Please see the comparison below.</span>
@ -580,16 +594,22 @@ export default function ProcessModelEditDiagram() {
</Col>
);
}
const inputJson = JSON.stringify(
JSON.parse(currentScriptUnitTest.inputJson.value),
null,
' '
);
const outputJson = JSON.stringify(
JSON.parse(currentScriptUnitTest.expectedOutputJson.value),
null,
' '
);
let inputJson = currentScriptUnitTest.inputJson.value;
let outputJson = currentScriptUnitTest.expectedOutputJson.value;
try {
inputJson = JSON.stringify(
JSON.parse(currentScriptUnitTest.inputJson.value),
null,
' '
);
outputJson = JSON.stringify(
JSON.parse(currentScriptUnitTest.expectedOutputJson.value),
null,
' '
);
} catch (e) {
// Attemping to format the json failed -- it's invalid.
}
return (
<main>