diff --git a/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py b/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py new file mode 100644 index 00000000..50fa7ee4 --- /dev/null +++ b/alembic/versions/1497926ddec1_add_deleted_environment_role_status.py @@ -0,0 +1,59 @@ +"""add deleted environment_role status + +Revision ID: 1497926ddec1 +Revises: e3d93f9caba7 +Create Date: 2019-10-04 10:44:54.198368 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "1497926ddec1" # pragma: allowlist secret +down_revision = "f50596c5ffbb" # pragma: allowlist secret +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column( + "environment_roles", + "status", + type_=sa.Enum( + "PENDING", + "COMPLETED", + "PENDING_DELETE", + "DELETED", + name="status", + native_enum=False, + ), + existing_type=sa.Enum( + "PENDING", "COMPLETED", "PENDING_DELETE", name="status", native_enum=False + ), + ) + + +def downgrade(): + conn = op.get_bind() + conn.execute( + """ + UPDATE environment_roles + SET status = (CASE WHEN status = 'DELETED' THEN 'PENDING_DELETE' ELSE status END) + """ + ) + op.alter_column( + "environment_roles", + "status", + type_=sa.Enum( + "PENDING", "COMPLETED", "PENDING_DELETE", name="status", native_enum=False + ), + existing_type=sa.Enum( + "PENDING", + "COMPLETED", + "PENDING_DELETE", + "DELETED", + name="status", + native_enum=False, + ), + ) diff --git a/atst/app.py b/atst/app.py index f3e7276f..31eb6f85 100644 --- a/atst/app.py +++ b/atst/app.py @@ -29,7 +29,7 @@ from atst.models.permissions import Permissions from atst.queue import celery, update_celery from atst.utils import mailer from atst.utils.form_cache import FormCache -from atst.utils.json import CustomJSONEncoder +from atst.utils.json import CustomJSONEncoder, sqlalchemy_dumps from atst.utils.notification_sender import NotificationSender from atst.utils.session_limiter import SessionLimiter @@ -158,6 +158,7 @@ def map_config(config): "PORT": int(config["default"]["PORT"]), "SQLALCHEMY_DATABASE_URI": config["default"]["DATABASE_URI"], "SQLALCHEMY_TRACK_MODIFICATIONS": False, + "SQLALCHEMY_ENGINE_OPTIONS": {"json_serializer": sqlalchemy_dumps}, "WTF_CSRF_ENABLED": config.getboolean("default", "WTF_CSRF_ENABLED"), "PERMANENT_SESSION_LIFETIME": config.getint( "default", "PERMANENT_SESSION_LIFETIME" diff --git a/atst/domain/environment_roles.py b/atst/domain/environment_roles.py index 9dd59365..bdf4ae09 100644 --- a/atst/domain/environment_roles.py +++ b/atst/domain/environment_roles.py @@ -56,10 +56,20 @@ class EnvironmentRoles(object): @classmethod def delete(cls, application_role_id, environment_id): - existing_env_role = EnvironmentRoles.get(application_role_id, environment_id) + existing_env_role = ( + db.session.query(EnvironmentRole) + .join(ApplicationRole) + .filter( + ApplicationRole.id == application_role_id, + EnvironmentRole.environment_id == environment_id, + EnvironmentRole.status != EnvironmentRole.Status.PENDING_DELETE, + ) + .one_or_none() + ) + if existing_env_role: - # TODO: Set status to pending_delete - db.session.delete(existing_env_role) + existing_env_role.status = EnvironmentRole.Status.PENDING_DELETE + existing_env_role.role = "deleted" db.session.commit() return True else: @@ -87,3 +97,17 @@ class EnvironmentRoles(object): .all() ) return [id_ for id_, in results] + + @classmethod + def get_environment_roles_pending_deletion(cls) -> List[UUID]: + results = ( + db.session.query(EnvironmentRole.id) + .join(Environment) + .join(ApplicationRole) + .filter(Environment.deleted == False) + .filter(Environment.baseline_info != None) + .filter(EnvironmentRole.status == EnvironmentRole.Status.PENDING_DELETE) + .filter(ApplicationRole.status == ApplicationRoleStatus.ACTIVE) + .all() + ) + return [id_ for id_, in results] diff --git a/atst/jobs.py b/atst/jobs.py index e861d8b5..2a3ea97f 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -3,7 +3,11 @@ import pendulum from atst.database import db from atst.queue import celery -from atst.models import EnvironmentJobFailure, EnvironmentRoleJobFailure +from atst.models import ( + EnvironmentJobFailure, + EnvironmentRoleJobFailure, + EnvironmentRole, +) from atst.domain.csp.cloud import CloudProviderInterface, GeneralCSPException from atst.domain.environments import Environments from atst.domain.environment_roles import EnvironmentRoles @@ -112,6 +116,20 @@ def do_provision_user(csp: CloudProviderInterface, environment_role_id=None): credentials, environment_role, environment_role.role ) environment_role.csp_user_id = csp_user_id + environment_role.status = EnvironmentRole.Status.COMPLETED + db.session.add(environment_role) + db.session.commit() + + +def do_delete_user(csp: CloudProviderInterface, environment_role_id=None): + environment_role = EnvironmentRoles.get_by_id(environment_role_id) + + with claim_for_update(environment_role) as environment_role: + credentials = environment_role.environment.csp_credentials + + csp.delete_user(credentials, environment_role.csp_user_id) + environment_role.status = EnvironmentRole.Status.DELETED + environment_role.deleted = True db.session.add(environment_role) db.session.commit() @@ -152,6 +170,13 @@ def provision_user(self, environment_role_id=None): ) +@celery.task(bind=True) +def delete_user(self, environment_role_id=None): + do_work( + do_delete_user, self, app.csp.cloud, environment_role_id=environment_role_id + ) + + @celery.task(bind=True) def dispatch_create_environment(self): for environment_id in Environments.get_environments_pending_creation( @@ -182,3 +207,11 @@ def dispatch_provision_user(self): environment_role_id ) in EnvironmentRoles.get_environment_roles_pending_creation(): provision_user.delay(environment_role_id=environment_role_id) + + +@celery.task(bind=True) +def dispatch_delete_user(self): + for ( + environment_role_id + ) in EnvironmentRoles.get_environment_roles_pending_deletion(): + delete_user.delay(environment_role_id=environment_role_id) diff --git a/atst/models/environment_role.py b/atst/models/environment_role.py index 988d19ab..f3e8004f 100644 --- a/atst/models/environment_role.py +++ b/atst/models/environment_role.py @@ -40,6 +40,7 @@ class EnvironmentRole( PENDING = "pending" COMPLETED = "completed" PENDING_DELETE = "pending_delete" + DELETED = "deleted" status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) diff --git a/atst/queue.py b/atst/queue.py index c7d117c6..3a68be54 100644 --- a/atst/queue.py +++ b/atst/queue.py @@ -22,6 +22,10 @@ def update_celery(celery, app): "task": "atst.jobs.dispatch_provision_user", "schedule": 60, }, + "beat-dispatch_delete_user": { + "task": "atst.jobs.dispatch_delete_user", + "schedule": 60, + }, } class ContextTask(celery.Task): diff --git a/atst/utils/json.py b/atst/utils/json.py index 8e2a3217..9e34e10e 100644 --- a/atst/utils/json.py +++ b/atst/utils/json.py @@ -1,6 +1,9 @@ from flask.json import JSONEncoder +import json from werkzeug.datastructures import FileStorage from datetime import date +from enum import Enum + from atst.models.attachment import Attachment @@ -13,3 +16,13 @@ class CustomJSONEncoder(JSONEncoder): elif isinstance(obj, FileStorage): return obj.filename return JSONEncoder.default(self, obj) + + +def sqlalchemy_dumps(dct): + def _default(obj): + if isinstance(obj, Enum): + return obj.name + else: + raise TypeError() + + return json.dumps(dct, default=_default) diff --git a/js/test_templates/clin_fields.html b/js/test_templates/clin_fields.html index abcd43f3..f0f51fc4 100644 --- a/js/test_templates/clin_fields.html +++ b/js/test_templates/clin_fields.html @@ -312,6 +312,8 @@ + + -