diff --git a/alembic/versions/f328f1ea400c_add_disabled_enviornment_role_status.py b/alembic/versions/f328f1ea400c_add_disabled_enviornment_role_status.py new file mode 100644 index 00000000..c0d73f8d --- /dev/null +++ b/alembic/versions/f328f1ea400c_add_disabled_enviornment_role_status.py @@ -0,0 +1,57 @@ +"""add disabled enviornment_role status + +Revision ID: f328f1ea400c +Revises: e05d1f2682af +Create Date: 2019-10-29 15:16:04.037436 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f328f1ea400c' # pragma: allowlist secret +down_revision = 'e05d1f2682af' # pragma: allowlist secret +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column( + "environment_roles", + "status", + type_=sa.Enum( + "PENDING", + "COMPLETED", + "DISABLED", + 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 = 'DISABLED' 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", + "DISABLED", + name="status", + native_enum=False, + ), + ) diff --git a/atst/domain/csp/__init__.py b/atst/domain/csp/__init__.py index e8fc5236..d886f8a2 100644 --- a/atst/domain/csp/__init__.py +++ b/atst/domain/csp/__init__.py @@ -6,7 +6,10 @@ from .reports import MockReportingProvider class MockCSP: def __init__(self, app, test_mode=False): self.cloud = MockCloudProvider( - app.config, with_delay=(not test_mode), with_failure=(not test_mode) + app.config, + with_delay=(not test_mode), + with_failure=(not test_mode), + with_authorization=(not test_mode), ) self.files = MockUploader(app) self.reports = MockReportingProvider() diff --git a/atst/domain/csp/cloud.py b/atst/domain/csp/cloud.py index 9726a14f..7bcbf44f 100644 --- a/atst/domain/csp/cloud.py +++ b/atst/domain/csp/cloud.py @@ -215,7 +215,7 @@ class CloudProviderInterface: """ raise NotImplementedError() - def suspend_user(self, auth_credentials: Dict, csp_user_id: str) -> bool: + def disable_user(self, auth_credentials: Dict, csp_user_id: str) -> bool: """Revoke all privileges for a user. Used to prevent user access while a full delete is being processed. @@ -235,25 +235,6 @@ class CloudProviderInterface: """ raise NotImplementedError() - def delete_user(self, auth_credentials: Dict, csp_user_id: str) -> bool: - """Given the csp-internal id for a user, initiate user deletion. - - Arguments: - auth_credentials -- Object containing CSP account credentials - csp_user_id -- CSP internal user identifier - - Returns: - bool -- True on success - - Raises: - AuthenticationException: Problem with the credentials - AuthorizationException: Credentials not authorized for current action(s) - ConnectionException: Issue with the CSP API connection - UnknownServerException: Unknown issue on the CSP side - UserRemovalException: User couldn't be removed - """ - raise NotImplementedError() - def get_calculator_url(self) -> str: """Returns the calculator url for the CSP. This will likely be a static property elsewhere once a CSP is chosen. @@ -281,12 +262,15 @@ class MockCloudProvider(CloudProviderInterface): ATAT_ADMIN_CREATE_FAILURE_PCT = 12 UNAUTHORIZED_RATE = 2 - def __init__(self, config, with_delay=True, with_failure=True): + def __init__( + self, config, with_delay=True, with_failure=True, with_authorization=True + ): from time import sleep import random self._with_delay = with_delay self._with_failure = with_failure + self._with_authorization = with_authorization self._sleep = sleep self._random = random @@ -356,31 +340,18 @@ class MockCloudProvider(CloudProviderInterface): self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) return self._id() - def suspend_user(self, auth_credentials, csp_user_id): + def disable_user(self, auth_credentials, csp_user_id): self._authorize(auth_credentials) self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) self._maybe_raise( self.ATAT_ADMIN_CREATE_FAILURE_PCT, - UserRemovalException(csp_user_id, "Could not suspend user."), + UserRemovalException(csp_user_id, "Could not disable user."), ) return self._maybe(12) - def delete_user(self, auth_credentials, csp_user_id): - self._authorize(auth_credentials) - self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) - self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) - - self._maybe_raise( - self.ATAT_ADMIN_CREATE_FAILURE_PCT, - UserRemovalException(csp_user_id, "Could not delete user."), - ) - - self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) - return self._maybe(12) - def get_calculator_url(self): return "https://www.rackspace.com/en-us/calculator" @@ -410,5 +381,5 @@ class MockCloudProvider(CloudProviderInterface): def _authorize(self, credentials): self._delay(1, 5) - if credentials != self._auth_credentials: + if self._with_authorization and credentials != self._auth_credentials: raise self.AUTHENTICATION_EXCEPTION diff --git a/atst/domain/environment_roles.py b/atst/domain/environment_roles.py index 016110a2..da73eee4 100644 --- a/atst/domain/environment_roles.py +++ b/atst/domain/environment_roles.py @@ -1,4 +1,5 @@ from sqlalchemy.orm.exc import NoResultFound +from flask import current_app as app from atst.database import db from atst.models import ( @@ -91,3 +92,16 @@ class EnvironmentRoles(object): .all() ) return [id_ for id_, in results] + + @classmethod + def disable(cls, environment_role_id): + environment_role = EnvironmentRoles.get_by_id(environment_role_id) + + credentials = environment_role.environment.csp_credentials + app.csp.cloud.disable_user(credentials, environment_role.csp_user_id) + + environment_role.status = EnvironmentRole.Status.DISABLED + db.session.add(environment_role) + db.session.commit() + + return environment_role diff --git a/atst/models/environment_role.py b/atst/models/environment_role.py index 988d19ab..613ade78 100644 --- a/atst/models/environment_role.py +++ b/atst/models/environment_role.py @@ -39,7 +39,7 @@ class EnvironmentRole( class Status(Enum): PENDING = "pending" COMPLETED = "completed" - PENDING_DELETE = "pending_delete" + DISABLED = "disabled" status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) diff --git a/tests/domain/cloud/test_mock_csp.py b/tests/domain/cloud/test_mock_csp.py index 93b8dfc8..b3a8d551 100644 --- a/tests/domain/cloud/test_mock_csp.py +++ b/tests/domain/cloud/test_mock_csp.py @@ -36,9 +36,5 @@ def test_create_or_update_user(mock_csp: MockCloudProvider): assert isinstance(csp_user_id, str) -def test_suspend_user(mock_csp: MockCloudProvider): - assert mock_csp.suspend_user(CREDENTIALS, "csp_user_id") - - -def test_delete_user(mock_csp: MockCloudProvider): - assert mock_csp.delete_user(CREDENTIALS, "csp_user_id") +def test_disable_user(mock_csp: MockCloudProvider): + assert mock_csp.disable_user(CREDENTIALS, "csp_user_id") diff --git a/tests/domain/test_environment_roles.py b/tests/domain/test_environment_roles.py index a8618981..30f7a32b 100644 --- a/tests/domain/test_environment_roles.py +++ b/tests/domain/test_environment_roles.py @@ -76,3 +76,15 @@ def test_get_for_application_member_does_not_return_deleted( roles = EnvironmentRoles.get_for_application_member(application_role.id) assert len(roles) == 0 + + +def test_disable_completed(application_role, environment): + environment_role = EnvironmentRoleFactory.create( + application_role=application_role, + environment=environment, + status=EnvironmentRole.Status.COMPLETED, + ) + + environment_role = EnvironmentRoles.disable(environment_role.id) + + assert environment_role.status == EnvironmentRole.Status.DISABLED