diff --git a/spiffworkflow-backend/bin/delete_and_import_all_permissions.py b/spiffworkflow-backend/bin/delete_and_import_all_permissions.py index a55e36e7f..966ec5a11 100644 --- a/spiffworkflow-backend/bin/delete_and_import_all_permissions.py +++ b/spiffworkflow-backend/bin/delete_and_import_all_permissions.py @@ -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__": diff --git a/spiffworkflow-backend/migrations/env.py b/spiffworkflow-backend/migrations/env.py index 630e381ad..68feded2a 100644 --- a/spiffworkflow-backend/migrations/env.py +++ b/spiffworkflow-backend/migrations/env.py @@ -1,3 +1,5 @@ +from __future__ import with_statement + import logging from logging.config import fileConfig diff --git a/spiffworkflow-backend/migrations/versions/e1d0d593c621_.py b/spiffworkflow-backend/migrations/versions/e1d0d593c621_.py deleted file mode 100644 index cbe697cc5..000000000 --- a/spiffworkflow-backend/migrations/versions/e1d0d593c621_.py +++ /dev/null @@ -1,331 +0,0 @@ -"""empty message - -Revision ID: e1d0d593c621 -Revises: -Create Date: 2022-12-12 14:23:44.643766 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'e1d0d593c621' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('group', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=255), nullable=True), - sa.Column('identifier', sa.String(length=255), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('message_model', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('identifier', sa.String(length=50), nullable=True), - sa.Column('name', sa.String(length=50), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_message_model_identifier'), 'message_model', ['identifier'], unique=True) - op.create_index(op.f('ix_message_model_name'), 'message_model', ['name'], unique=True) - op.create_table('permission_target', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('uri', sa.String(length=255), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('uri') - ) - op.create_table('spec_reference_cache', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('identifier', sa.String(length=255), nullable=True), - sa.Column('display_name', sa.String(length=255), nullable=True), - sa.Column('process_model_id', sa.String(length=255), nullable=True), - sa.Column('type', sa.String(length=255), nullable=True), - sa.Column('file_name', sa.String(length=255), nullable=True), - sa.Column('relative_path', sa.String(length=255), nullable=True), - sa.Column('has_lanes', sa.Boolean(), nullable=True), - sa.Column('is_executable', sa.Boolean(), nullable=True), - sa.Column('is_primary', sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('identifier', 'type', name='_identifier_type_unique') - ) - op.create_index(op.f('ix_spec_reference_cache_display_name'), 'spec_reference_cache', ['display_name'], unique=False) - op.create_index(op.f('ix_spec_reference_cache_identifier'), 'spec_reference_cache', ['identifier'], unique=False) - op.create_index(op.f('ix_spec_reference_cache_type'), 'spec_reference_cache', ['type'], unique=False) - op.create_table('spiff_logging', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('process_instance_id', sa.Integer(), nullable=False), - sa.Column('bpmn_process_identifier', sa.String(length=255), nullable=False), - sa.Column('bpmn_task_identifier', sa.String(length=255), nullable=False), - sa.Column('bpmn_task_name', sa.String(length=255), nullable=True), - sa.Column('bpmn_task_type', sa.String(length=255), nullable=True), - sa.Column('spiff_task_guid', sa.String(length=50), nullable=False), - sa.Column('timestamp', sa.DECIMAL(precision=17, scale=6), nullable=False), - sa.Column('message', sa.String(length=255), nullable=True), - sa.Column('current_user_id', sa.Integer(), nullable=True), - sa.Column('spiff_step', sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('username', sa.String(length=255), nullable=False), - sa.Column('service', sa.String(length=50), nullable=False), - sa.Column('service_id', sa.String(length=255), nullable=False), - sa.Column('email', sa.String(length=255), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('service', 'service_id', name='service_key'), - sa.UniqueConstraint('username') - ) - op.create_table('message_correlation_property', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('identifier', sa.String(length=50), nullable=True), - sa.Column('message_model_id', sa.Integer(), nullable=False), - sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), - sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['message_model_id'], ['message_model.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('identifier', 'message_model_id', name='message_correlation_property_unique') - ) - op.create_index(op.f('ix_message_correlation_property_identifier'), 'message_correlation_property', ['identifier'], unique=False) - op.create_table('message_triggerable_process_model', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('message_model_id', sa.Integer(), nullable=False), - sa.Column('process_model_identifier', sa.String(length=50), nullable=False), - sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), - sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['message_model_id'], ['message_model.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('message_model_id') - ) - op.create_index(op.f('ix_message_triggerable_process_model_process_model_identifier'), 'message_triggerable_process_model', ['process_model_identifier'], unique=False) - op.create_table('principal', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('group_id', sa.Integer(), nullable=True), - sa.CheckConstraint('NOT(user_id IS NULL AND group_id IS NULL)'), - sa.ForeignKeyConstraint(['group_id'], ['group.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('group_id'), - sa.UniqueConstraint('user_id') - ) - op.create_table('process_instance', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('process_model_identifier', sa.String(length=255), nullable=False), - sa.Column('process_model_display_name', sa.String(length=255), nullable=False), - sa.Column('process_initiator_id', sa.Integer(), nullable=False), - sa.Column('bpmn_json', sa.JSON(), nullable=True), - sa.Column('start_in_seconds', sa.Integer(), nullable=True), - sa.Column('end_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('status', sa.String(length=50), nullable=True), - sa.Column('bpmn_version_control_type', sa.String(length=50), nullable=True), - sa.Column('bpmn_version_control_identifier', sa.String(length=255), nullable=True), - sa.Column('spiff_step', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['process_initiator_id'], ['user.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_process_instance_process_model_display_name'), 'process_instance', ['process_model_display_name'], unique=False) - op.create_index(op.f('ix_process_instance_process_model_identifier'), 'process_instance', ['process_model_identifier'], unique=False) - op.create_table('process_instance_report', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('identifier', sa.String(length=50), nullable=False), - sa.Column('report_metadata', sa.JSON(), nullable=True), - sa.Column('created_by_id', sa.Integer(), nullable=False), - sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), - sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('created_by_id', 'identifier', name='process_instance_report_unique') - ) - op.create_index(op.f('ix_process_instance_report_created_by_id'), 'process_instance_report', ['created_by_id'], unique=False) - op.create_index(op.f('ix_process_instance_report_identifier'), 'process_instance_report', ['identifier'], unique=False) - op.create_table('refresh_token', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('token', sa.String(length=1024), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('user_id') - ) - op.create_table('secret', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('key', sa.String(length=50), nullable=False), - sa.Column('value', sa.Text(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), - sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('key') - ) - op.create_table('user_group_assignment', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('group_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['group_id'], ['group.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('user_id', 'group_id', name='user_group_assignment_unique') - ) - op.create_table('active_task', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('process_instance_id', sa.Integer(), nullable=False), - sa.Column('actual_owner_id', sa.Integer(), nullable=True), - sa.Column('lane_assignment_id', sa.Integer(), nullable=True), - sa.Column('form_file_name', sa.String(length=50), nullable=True), - sa.Column('ui_form_file_name', sa.String(length=50), nullable=True), - sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), - sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), - sa.Column('task_id', sa.String(length=50), nullable=True), - sa.Column('task_name', sa.String(length=50), nullable=True), - sa.Column('task_title', sa.String(length=50), nullable=True), - sa.Column('task_type', sa.String(length=50), nullable=True), - sa.Column('task_status', sa.String(length=50), nullable=True), - sa.Column('process_model_display_name', sa.String(length=255), nullable=True), - sa.ForeignKeyConstraint(['actual_owner_id'], ['user.id'], ), - sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ), - sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('task_id', 'process_instance_id', name='active_task_unique') - ) - op.create_table('message_correlation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('process_instance_id', sa.Integer(), nullable=False), - sa.Column('message_correlation_property_id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('value', sa.String(length=255), nullable=False), - sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), - sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['message_correlation_property_id'], ['message_correlation_property.id'], ), - sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('process_instance_id', 'message_correlation_property_id', 'name', name='message_instance_id_name_unique') - ) - op.create_index(op.f('ix_message_correlation_message_correlation_property_id'), 'message_correlation', ['message_correlation_property_id'], unique=False) - op.create_index(op.f('ix_message_correlation_name'), 'message_correlation', ['name'], unique=False) - op.create_index(op.f('ix_message_correlation_process_instance_id'), 'message_correlation', ['process_instance_id'], unique=False) - op.create_index(op.f('ix_message_correlation_value'), 'message_correlation', ['value'], unique=False) - op.create_table('message_instance', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('process_instance_id', sa.Integer(), nullable=False), - sa.Column('message_model_id', sa.Integer(), nullable=False), - sa.Column('message_type', sa.String(length=20), nullable=False), - sa.Column('payload', sa.JSON(), nullable=True), - sa.Column('status', sa.String(length=20), nullable=False), - sa.Column('failure_cause', sa.Text(), nullable=True), - sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), - sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['message_model_id'], ['message_model.id'], ), - sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('permission_assignment', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('principal_id', sa.Integer(), nullable=False), - sa.Column('permission_target_id', sa.Integer(), nullable=False), - sa.Column('grant_type', sa.String(length=50), nullable=False), - sa.Column('permission', sa.String(length=50), nullable=False), - sa.ForeignKeyConstraint(['permission_target_id'], ['permission_target.id'], ), - sa.ForeignKeyConstraint(['principal_id'], ['principal.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('principal_id', 'permission_target_id', 'permission', name='permission_assignment_uniq') - ) - op.create_table('process_instance_metadata', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('process_instance_id', sa.Integer(), nullable=False), - sa.Column('key', sa.String(length=255), nullable=False), - sa.Column('value', sa.String(length=255), nullable=False), - sa.Column('updated_at_in_seconds', sa.Integer(), nullable=False), - sa.Column('created_at_in_seconds', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('process_instance_id', 'key', name='process_instance_metadata_unique') - ) - op.create_index(op.f('ix_process_instance_metadata_key'), 'process_instance_metadata', ['key'], unique=False) - op.create_table('spiff_step_details', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('process_instance_id', sa.Integer(), nullable=False), - sa.Column('spiff_step', sa.Integer(), nullable=False), - sa.Column('task_json', sa.JSON(), nullable=False), - sa.Column('timestamp', sa.DECIMAL(precision=17, scale=6), nullable=False), - sa.Column('completed_by_user_id', sa.Integer(), nullable=True), - sa.Column('lane_assignment_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['lane_assignment_id'], ['group.id'], ), - sa.ForeignKeyConstraint(['process_instance_id'], ['process_instance.id'], ), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('active_task_user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('active_task_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['active_task_id'], ['active_task.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('active_task_id', 'user_id', name='active_task_user_unique') - ) - op.create_index(op.f('ix_active_task_user_active_task_id'), 'active_task_user', ['active_task_id'], unique=False) - op.create_index(op.f('ix_active_task_user_user_id'), 'active_task_user', ['user_id'], unique=False) - op.create_table('message_correlation_message_instance', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('message_instance_id', sa.Integer(), nullable=False), - sa.Column('message_correlation_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['message_correlation_id'], ['message_correlation.id'], ), - sa.ForeignKeyConstraint(['message_instance_id'], ['message_instance.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('message_instance_id', 'message_correlation_id', name='message_correlation_message_instance_unique') - ) - op.create_index(op.f('ix_message_correlation_message_instance_message_correlation_id'), 'message_correlation_message_instance', ['message_correlation_id'], unique=False) - op.create_index(op.f('ix_message_correlation_message_instance_message_instance_id'), 'message_correlation_message_instance', ['message_instance_id'], unique=False) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_message_correlation_message_instance_message_instance_id'), table_name='message_correlation_message_instance') - op.drop_index(op.f('ix_message_correlation_message_instance_message_correlation_id'), table_name='message_correlation_message_instance') - op.drop_table('message_correlation_message_instance') - op.drop_index(op.f('ix_active_task_user_user_id'), table_name='active_task_user') - op.drop_index(op.f('ix_active_task_user_active_task_id'), table_name='active_task_user') - op.drop_table('active_task_user') - op.drop_table('spiff_step_details') - op.drop_index(op.f('ix_process_instance_metadata_key'), table_name='process_instance_metadata') - op.drop_table('process_instance_metadata') - op.drop_table('permission_assignment') - op.drop_table('message_instance') - op.drop_index(op.f('ix_message_correlation_value'), table_name='message_correlation') - op.drop_index(op.f('ix_message_correlation_process_instance_id'), table_name='message_correlation') - op.drop_index(op.f('ix_message_correlation_name'), table_name='message_correlation') - op.drop_index(op.f('ix_message_correlation_message_correlation_property_id'), table_name='message_correlation') - op.drop_table('message_correlation') - op.drop_table('active_task') - op.drop_table('user_group_assignment') - op.drop_table('secret') - op.drop_table('refresh_token') - op.drop_index(op.f('ix_process_instance_report_identifier'), table_name='process_instance_report') - op.drop_index(op.f('ix_process_instance_report_created_by_id'), table_name='process_instance_report') - op.drop_table('process_instance_report') - op.drop_index(op.f('ix_process_instance_process_model_identifier'), table_name='process_instance') - op.drop_index(op.f('ix_process_instance_process_model_display_name'), table_name='process_instance') - op.drop_table('process_instance') - op.drop_table('principal') - op.drop_index(op.f('ix_message_triggerable_process_model_process_model_identifier'), table_name='message_triggerable_process_model') - op.drop_table('message_triggerable_process_model') - op.drop_index(op.f('ix_message_correlation_property_identifier'), table_name='message_correlation_property') - op.drop_table('message_correlation_property') - op.drop_table('user') - op.drop_table('spiff_logging') - op.drop_index(op.f('ix_spec_reference_cache_type'), table_name='spec_reference_cache') - op.drop_index(op.f('ix_spec_reference_cache_identifier'), table_name='spec_reference_cache') - op.drop_index(op.f('ix_spec_reference_cache_display_name'), table_name='spec_reference_cache') - op.drop_table('spec_reference_cache') - op.drop_table('permission_target') - op.drop_index(op.f('ix_message_model_name'), table_name='message_model') - op.drop_index(op.f('ix_message_model_identifier'), table_name='message_model') - op.drop_table('message_model') - op.drop_table('group') - # ### end Alembic commands ### diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example.yml index 493aab1f1..5bf57f1ac 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example.yml @@ -21,17 +21,17 @@ groups: admin: users: [ - admin, + admin@spiffworkflow.org, ] Education: users: [ - malala + malala@spiffworkflow.org ] President: users: [ - nelson + nelson@spiffworkflow.org ] permissions: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py index 3b7edd6ce..eb9d9e1ca 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/group.py @@ -27,6 +27,7 @@ class GroupModel(FlaskBpmnGroupModel): identifier = db.Column(db.String(255)) user_group_assignments = relationship("UserGroupAssignmentModel", cascade="delete") + user_group_assignments_waiting = relationship("UserGroupAssignmentWaitingModel", cascade="delete") users = relationship( # type: ignore "UserModel", viewonly=True, diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py index 60d40e9d3..6c1cc1356 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/user.py @@ -29,9 +29,10 @@ class UserModel(SpiffworkflowBaseDBModel): __tablename__ = "user" __table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),) id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(255), nullable=False, unique=True) # should always be an email address. + 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) + display_name = db.Column(db.String(255)) email = db.Column(db.String(255)) user_group_assignments = relationship("UserGroupAssignmentModel", cascade="delete") # type: ignore diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/user_group_assignment_waiting.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/user_group_assignment_waiting.py new file mode 100644 index 000000000..78f811a0a --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/user_group_assignment_waiting.py @@ -0,0 +1,24 @@ +"""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 +from spiffworkflow_backend.models.user import UserModel + + +class UserGroupAssignmentWaitingModel(SpiffworkflowBaseDBModel): + """UserGroupAssignmentsWaitingModel - When a user is assinged to a group, but that user has not yet logged into + the system, this caches that assignment, so it can be applied at the time the user logs in.""" + + __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_assignment_waiting,users") # type: ignore diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/admin_blueprint/admin_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/admin_blueprint/admin_blueprint.py index f1223ae0d..5cb0ae89b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/admin_blueprint/admin_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/admin_blueprint/admin_blueprint.py @@ -141,7 +141,7 @@ def process_model_save(process_model_id: str, file_name: str) -> Union[str, Resp @admin_blueprint.route("/process-models//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 diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py index 88e55cbaa..a690a1606 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py @@ -341,5 +341,5 @@ def get_user_from_decoded_internal_token(decoded_token: dict) -> Optional[UserMo ) if user: return user - user = UserService.create_user(service, service_id, username=service_id) + user = UserService.create_user(service_id, service, service_id) return user diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/add_permission.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/add_permission.py new file mode 100644 index 000000000..6c9d97a53 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/add_permission.py @@ -0,0 +1,38 @@ +"""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.authorization_service import AuthorizationService +from spiffworkflow_backend.services.group_service import GroupService +from spiffworkflow_backend.services.user_service import UserService + +# add_permission("read", "test/*", "Editors") +class AddPermission(Script): + """AddUserToGroup.""" + + def get_description(self) -> str: + """Get_description.""" + return """Add a permission to a group. ex: add_permission("read", "test/*", "Editors") """ + + def run( + self, + script_attributes_context: ScriptAttributesContext, + *args: Any, + **kwargs: Any, + ) -> Any: + """Run.""" + allowed_permission = args[0] + uri = args[1] + group_identifier = args[2] + group = GroupService.find_or_create_group(group_identifier) + target = AuthorizationService.find_or_create_permission_target(uri) + AuthorizationService.create_permission_for_principal( + group.principal, target, allowed_permission + ) \ No newline at end of file diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/add_user_to_group.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/add_user_to_group.py index d3c777118..3342b5c02 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/add_user_to_group.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/add_user_to_group.py @@ -9,6 +9,7 @@ from spiffworkflow_backend.models.script_attributes_context import ( 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.group_service import GroupService from spiffworkflow_backend.services.user_service import UserService @@ -17,7 +18,7 @@ class AddUserToGroup(Script): def get_description(self) -> str: """Get_description.""" - return """Add a given user to a given group.""" + return """Add a given user to a given group. Ex. add_user_to_group(group='Education', service_id='1234123')""" def run( self, @@ -28,16 +29,10 @@ class AddUserToGroup(Script): """Run.""" username = args[0] group_identifier = args[1] + + group = GroupService.find_or_create_group(group_identifier) 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) + if user: + UserService.add_user_to_group(user, group) + else: + UserService.add_waiting_group_assignment(username, group) \ No newline at end of file diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/clear_permissions.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/clear_permissions.py new file mode 100644 index 000000000..b210a6723 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/clear_permissions.py @@ -0,0 +1,30 @@ +"""Get_env.""" +from typing import Any +from flask_bpmn.models.db import db +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.authorization_service import AuthorizationService +from spiffworkflow_backend.services.user_service import UserService + + +class ClearPermissions(Script): + """Clear all permissions across the system .""" + + def get_description(self) -> str: + """Get_description.""" + return """Remove all groups and permissions from the database.""" + + def run( + self, + script_attributes_context: ScriptAttributesContext, + *args: Any, + **kwargs: Any, + ) -> Any: + """Run.""" + AuthorizationService.delete_all_permissions() \ No newline at end of file diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authentication_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authentication_service.py index b0e61f372..fd2bdb898 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authentication_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authentication_service.py @@ -93,7 +93,7 @@ class AuthenticationService: + f"?state={state}&" + "response_type=code&" + f"client_id={self.client_id()}&" - + "scope=openid email&" + + "scope=openid profile email&" + f"redirect_uri={return_redirect_url}" ) return login_redirect_url diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index f32ad789f..1ad50b1c4 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -127,17 +127,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: @@ -193,14 +191,7 @@ class AuthorizationService: "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: @@ -226,6 +217,18 @@ class AuthorizationService: for user in UserModel.query.all(): cls.associate_user_with_group(user, default_group) + @classmethod + def find_or_create_permission_target(cls, 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() + return permission_target + @classmethod def create_permission_for_principal( cls, @@ -449,40 +452,46 @@ 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 - if user_info.get('email', None) is not None: - user_model = ( - UserModel.query.filter(UserModel.email == user_info["email"]).first() - ) - else: - user_model = ( - UserModel.query.filter(UserModel.service == user_info["iss"]) - .filter(UserModel.service_id == user_info["sub"]) - .first() - ) - username = email = "" - if "name" in user_info: - username = user_info["name"] - if "username" in user_info: - username = user_info["username"] - elif "preferred_username" in user_info: - username = user_info["preferred_username"] + user_model = ( + 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 user_model = UserService().create_user( + username=username, service=user_info["iss"], service_id=user_info["sub"], - username=username, email=email, + display_name = display_name ) + UserService().apply_waiting_group_assignments(user_model) + 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"] diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 5edc526cf..6d110a7dc 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -151,6 +151,7 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore "time": time, "decimal": decimal, "_strptime": _strptime, + "enumerate": enumerate, } # This will overwrite the standard builtins diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py index b5898c13f..720d9ff8e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/user_service.py @@ -13,6 +13,7 @@ from spiffworkflow_backend.models.group import GroupModel 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,10 +22,11 @@ class UserService: @classmethod def create_user( cls, + username: str, service: str, service_id: str, - username: Optional[str] = "", email: Optional[str] = "", + display_name: Optional[str] = "" ) -> UserModel: """Create_user.""" user_model: Optional[UserModel] = ( @@ -41,6 +43,7 @@ class UserService: service=service, service_id=service_id, email=email, + display_name=display_name ) db.session.add(user_model) @@ -124,8 +127,26 @@ 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: + exists = UserGroupAssignmentWaitingModel().query.filter_by(username=username).filter_by(group_id=group.id).count() + if not exists: + wugam = UserGroupAssignmentWaitingModel(username=username, group_id=group.id) + db.session.add(wugam) + db.session.commit() + + @classmethod + def apply_waiting_group_assignments(cls, user: UserModel) -> None: + 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) db.session.commit() @staticmethod diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index 48982fc60..1084cc6d6 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -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 diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_add_permission.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_add_permission.py new file mode 100644 index 000000000..91546735d --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_add_permission.py @@ -0,0 +1,61 @@ +"""Test_get_localtime.""" +from flask.app import Flask +from flask.testing import FlaskClient +from flask_bpmn.models.db import db + +from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel +from spiffworkflow_backend.models.permission_target import PermissionTargetModel +from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext +from spiffworkflow_backend.scripts.add_permission import AddPermission +from spiffworkflow_backend.scripts.clear_permissions import ClearPermissions +from spiffworkflow_backend.services.authorization_service import AuthorizationService +from spiffworkflow_backend.services.group_service import GroupService +from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec + +from spiffworkflow_backend.models.group import GroupModel +from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.process_instance_processor import ( + ProcessInstanceProcessor, +) +from spiffworkflow_backend.services.user_service import UserService + + +class TestAddPermission(BaseTest): + """TestAddPermission.""" + + def test_can_add_permission ( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_can_get_members_of_a_group.""" + test_user = 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", + ) + + group = GroupModel.query.filter(GroupModel.identifier == 'my_test_group').first() + permission_target = PermissionTargetModel.query.filter(PermissionTargetModel.uri == '/test_add_permission/%').first() + assert(group is None) + assert(permission_target is None) + + result = AddPermission().run( + script_attributes_context, + "read", + "/test_add_permission/*", + "my_test_group" + ) + group = GroupModel.query.filter(GroupModel.identifier == 'my_test_group').first() + permission_target = PermissionTargetModel.query.filter(PermissionTargetModel.uri == '/test_add_permission/%').first() + permission_assignments = PermissionAssignmentModel.query.filter(PermissionAssignmentModel.principal_id == group.principal.id).all() + assert(group is not None) + assert(permission_target is not None) + assert(len(permission_assignments) == 1) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_add_user_to_group.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_add_user_to_group.py new file mode 100644 index 000000000..0f26e9c3d --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_add_user_to_group.py @@ -0,0 +1,68 @@ +"""Test_get_localtime.""" +from flask.app import Flask +from flask.testing import FlaskClient +from flask_bpmn.models.db import db + +from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext +from spiffworkflow_backend.models.user_group_assignment_waiting import UserGroupAssignmentWaitingModel +from spiffworkflow_backend.scripts.add_user_to_group import AddUserToGroup +from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec + +from spiffworkflow_backend.models.group import GroupModel +from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.process_instance_processor import ( + ProcessInstanceProcessor, +) +from spiffworkflow_backend.services.user_service import UserService + + +class TestAddUserToGroup(BaseTest): + """TestGetGroupMembers.""" + + def test_can_add_existing_user_to_existing_group( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_can_get_members_of_a_group.""" + my_user = self.find_or_create_user("my_user") + my_group = GroupModel(identifier="my_group") + db.session.add(my_group) + script_attributes_context = ScriptAttributesContext( + task=None, + environment_identifier="testing", + process_instance_id=1, + process_model_identifier="my_test_user", + ) + result = AddUserToGroup().run( + script_attributes_context, + my_user.username, + my_group.identifier + ) + assert(my_user in my_group.users) + + def test_can_add_non_existent_user_to_non_existent_group( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + script_attributes_context = ScriptAttributesContext( + task=None, + environment_identifier="testing", + process_instance_id=1, + process_model_identifier="my_test_user", + ) + result = AddUserToGroup().run( + script_attributes_context, + "dan@sartography.com", + "competent-joes" + ) + my_group = GroupModel.query.filter(GroupModel.identifier == "competent-joes").first() + assert (my_group is not None) + waiting_assignments = UserGroupAssignmentWaitingModel().query.filter_by(username="dan@sartography.com").first() + assert (waiting_assignments is not None) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_delete_permissions.py b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_delete_permissions.py new file mode 100644 index 000000000..3fdec995c --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/scripts/test_delete_permissions.py @@ -0,0 +1,64 @@ +"""Test_get_localtime.""" +from flask.app import Flask +from flask.testing import FlaskClient +from flask_bpmn.models.db import db + +from spiffworkflow_backend.models.permission_target import PermissionTargetModel +from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext +from spiffworkflow_backend.scripts.clear_permissions import ClearPermissions +from spiffworkflow_backend.services.authorization_service import AuthorizationService +from spiffworkflow_backend.services.group_service import GroupService +from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec + +from spiffworkflow_backend.models.group import GroupModel +from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.process_instance_processor import ( + ProcessInstanceProcessor, +) +from spiffworkflow_backend.services.user_service import UserService + + +class TestDeletePermissions(BaseTest): + """TestGetGroupMembers.""" + + def test_can_delete_members ( + self, + app: Flask, + client: FlaskClient, + with_db_and_bpmn_file_cleanup: None, + with_super_admin_user: UserModel, + ) -> None: + """Test_can_get_members_of_a_group.""" + initiator_user = self.find_or_create_user("initiator_user") + testuser1 = self.find_or_create_user("testuser1") + testuser2 = self.find_or_create_user("testuser2") + testuser3 = self.find_or_create_user("testuser3") + group_a = GroupService.find_or_create_group('groupA') + group_b = GroupService.find_or_create_group('groupB') + db.session.add(group_a) + db.session.add(group_b) + db.session.commit() + UserService.add_user_to_group(testuser1, group_a) + UserService.add_user_to_group(testuser2, group_a) + UserService.add_user_to_group(testuser3, group_b) + + target = PermissionTargetModel('test/*') + db.session.add(target) + db.session.commit() + AuthorizationService.create_permission_for_principal(group_a.principal, + target, + "read") + # 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", + ) + result = ClearPermissions().run( + script_attributes_context + ) + + groups = GroupModel.query.all() + assert(0 == len(groups)) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index 11108cd65..f0fe1fff0 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -134,7 +134,7 @@ class TestAuthorizationService(BaseTest): active_task.task_name, processor.bpmn_process_instance ) finance_user = AuthorizationService.create_user_from_sign_in( - {"username": "testuser2", "sub": "open_id", "iss": "https://test.stuff"} + {"username": "testuser2", "sub": "testuser2", "iss": "https://test.stuff", "email": "testuser2"} ) ProcessInstanceService.complete_form_task( processor, spiff_task, {}, finance_user, active_task