Skip to content

Commit

Permalink
Add trigger on revoking all access (#408)
Browse files Browse the repository at this point in the history
  • Loading branch information
GDay authored Nov 22, 2023
1 parent 21b531c commit 2220b81
Show file tree
Hide file tree
Showing 17 changed files with 233 additions and 5 deletions.
14 changes: 11 additions & 3 deletions back/admin/integrations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,10 @@ def headers(self, headers=None):
return new_headers

def user_exists(self, new_hire):
from users.models import IntegrationUser

# check if user has been created manually
if self.skip_user_provisioning:
from users.models import IntegrationUser

try:
user_integration = IntegrationUser.objects.get(
user=new_hire, integration=self
Expand All @@ -337,7 +337,15 @@ def user_exists(self, new_hire):
if not success:
return None

return self._replace_vars(self.manifest["exists"]["expected"]) in response.text
user_exists = (
self._replace_vars(self.manifest["exists"]["expected"]) in response.text
)

IntegrationUser.objects.update_or_create(
integration=self, user=new_hire, defaults={"revoked": not user_exists}
)

return user_exists

def needs_user_info(self, user):
if self.skip_user_provisioning:
Expand Down
7 changes: 7 additions & 0 deletions back/admin/integrations/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from admin.integrations.utils import get_value_from_notation
from organization.models import Notification
from users.factories import IntegrationUserFactory
from users.models import IntegrationUser


@pytest.mark.django_db
Expand Down Expand Up @@ -345,6 +346,9 @@ def test_integration_user_exists(
Mock(return_value=(True, Mock(text="[{'error': 'not_found'}]"))),
):
exists = integration.user_exists(new_hire)
assert IntegrationUser.objects.filter(
user=new_hire, integration=integration, revoked=True
).exists()
assert not exists

# Found user
Expand All @@ -353,6 +357,9 @@ def test_integration_user_exists(
Mock(return_value=(True, Mock(text="[{'user': '" + new_hire.email + "'}]"))),
):
exists = integration.user_exists(new_hire)
assert IntegrationUser.objects.filter(
user=new_hire, integration=integration, revoked=False
).exists()
assert exists

# Error went wrong
Expand Down
2 changes: 2 additions & 0 deletions back/admin/people/templates/new_hire_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ <h4 class="card-title">
{% endfor %}
</div>
</h4>
{% elif condition.condition_type == ConditionType.INTEGRATIONS_REVOKED %}
{% blocktranslate %}When all access has been revoked{% endblocktranslate %}
{% endif %}

</div>
Expand Down
2 changes: 2 additions & 0 deletions back/admin/people/templates/offboarding_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ <h4 class="card-title">
{% endfor %}
</div>
</h4>
{% elif condition.condition_type == ConditionType.INTEGRATIONS_REVOKED %}
{% blocktranslate %}When all access has been revoked{% endblocktranslate %}
{% elif condition.condition_type == ConditionType.TODO %}
<h4 class="card-title">
{% trans "When these tasks are completed:" %}
Expand Down
7 changes: 7 additions & 0 deletions back/admin/people/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,13 @@ def form_valid(self, form):
# delete all previous conditions (from being a new hire)
employee.conditions.all().delete()

# TODO: should become a background worker at some point
for integration in Integration.objects.filter(
manifest_type=Integration.ManifestType.WEBHOOK,
manifest__exists__isnull=False,
):
integration.user_exists(employee)

sequences = Sequence.offboarding.filter(id__in=sequence_ids)
employee.add_sequences(sequences)

Expand Down
7 changes: 7 additions & 0 deletions back/admin/sequences/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ class Meta:
model = Condition


class ConditionIntegrationsRevokedFactory(factory.django.DjangoModelFactory):
condition_type = Condition.Type.INTEGRATIONS_REVOKED

class Meta:
model = Condition


class SequenceFactory(factory.django.DjangoModelFactory):
name = FuzzyText()
category = Sequence.Category.ONBOARDING
Expand Down
1 change: 1 addition & 0 deletions back/admin/sequences/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def __init__(self, *args, **kwargs):
(2, _("On/Before employee's last day")),
(1, _("Based on one or more to do items")),
(4, _("Based on one or more admin tasks")),
(5, _("When all integrations have been revoked")),
]
self.fields["days"].help_text = _("Enter 0 for the last day")
self.fields["days"].label = _("Amount of days before")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.2.5 on 2023-11-21 00:32

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("sequences", "0044_alter_externalmessage_person_type"),
]

operations = [
migrations.AlterField(
model_name="condition",
name="condition_type",
field=models.IntegerField(
choices=[
(0, "After new hire has started"),
(1, "Based on one or more to do item(s)"),
(2, "Before the new hire has started"),
(3, "Without trigger"),
(4, "Based on one or more admin tasks"),
(5, "When all integrations have been revoked"),
],
default=0,
verbose_name="Block type",
),
),
]
9 changes: 9 additions & 0 deletions back/admin/sequences/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ def assign_to_user(self, user):
# We found our match. Amount matches AND the admin_tasks match
user_condition = condition
break

elif (
sequence_condition.condition_type == Condition.Type.INTEGRATIONS_REVOKED
):
user_condition = user.conditions.filter(
condition_type=Condition.Type.INTEGRATIONS_REVOKED
).first()

else:
# Condition (always just one) that will be assigned directly (type == 3)
# Just run the condition with the new hire
Expand Down Expand Up @@ -710,6 +718,7 @@ class Type(models.IntegerChoices):
BEFORE = 2, _("Before the new hire has started")
WITHOUT = 3, _("Without trigger")
ADMIN_TASK = 4, _("Based on one or more admin tasks")
INTEGRATIONS_REVOKED = 5, _("When all integrations have been revoked")

sequence = models.ForeignKey(
Sequence, on_delete=models.CASCADE, null=True, related_name="conditions"
Expand Down
2 changes: 2 additions & 0 deletions back/admin/sequences/templates/_sequence_condition.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ <h4 class="card-title">
{% else %} {# offboarding sequence #}
{% blocktranslate with days=condition.days time=condition.time %}{{ days }} workday(s) before termination at {{ time }}{% endblocktranslate %}
{% endif %}
{% elif condition.condition_type == ConditionType.INTEGRATIONS_REVOKED %}
{% blocktranslate %}When all access has been revoked{% endblocktranslate %}
{% else %}
{% translate "Without any trigger - assign directly" %}
{% endif %}
Expand Down
14 changes: 13 additions & 1 deletion back/admin/sequences/templates/sequence.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@
</li>
{% endif %}
{% endfor %}

{% for condition in conditions %}
{% if condition.condition_type == ConditionType.INTEGRATIONS_REVOKED %}
<li>
<div class="list-timeline-icon">
</div>
<div class="list-timeline-content pt-0" style="margin-top: -5px" id="condition-{{condition.id}}">
{% include '_sequence_condition.html' %}
</div>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
Expand Down Expand Up @@ -351,7 +363,7 @@ <h5 class="modal-title">{% translate "Item" %}</h5>
var dragged;
var draggedToElement;

const targetClassConditionsMap =
const targetClassConditionsMap =
{
before: (type) => allowedBefore.includes(type),
after: (type) => allowedAfter.includes(type),
Expand Down
41 changes: 41 additions & 0 deletions back/admin/sequences/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,47 @@ def test_sequence_assign_to_user_merge_admin_task_condition(
assert new_hire.conditions.all().count() == 2


@pytest.mark.django_db
def test_sequence_assign_to_user_merge_integrations_revoked_condition(
sequence_factory,
new_hire_factory,
condition_integrations_revoked_factory,
to_do_factory,
pending_admin_task_factory,
):
# Condition should merge as the condition admin_tasks match with an existing one

new_hire = new_hire_factory()
sequence = sequence_factory()
condition = condition_integrations_revoked_factory(sequence=sequence)

# Condition has two admin task items and will trigger one todo task
pending_admin_task1 = pending_admin_task_factory()
pending_admin_task2 = pending_admin_task_factory()
condition.condition_admin_tasks.set([pending_admin_task1, pending_admin_task2])
to_do1 = to_do_factory()
condition.to_do.add(to_do1)

# Add to new hire
new_hire.add_sequences([sequence])

# there is now one condition
assert new_hire.conditions.all().count() == 1

to_do2 = to_do_factory(template=False)
to_do3 = to_do_factory()

condition.to_do.add(to_do2)
condition.to_do.add(to_do3)

# Add again to new hire
new_hire.add_sequences([sequence])

# Condition item was updated and not a new one created
assert new_hire.conditions.all().count() == 1
assert new_hire.conditions.all().first().to_do.count() == 3


@pytest.mark.django_db
def test_sequence_add_unconditional_item(
sequence_factory,
Expand Down
2 changes: 2 additions & 0 deletions back/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from admin.sequences.factories import (
ConditionAdminTaskFactory,
ConditionIntegrationsRevokedFactory,
ConditionTimedFactory,
ConditionToDoFactory,
ConditionWithItemsFactory,
Expand Down Expand Up @@ -102,6 +103,7 @@ def run_around_tests(request, settings):
register(ConditionTimedFactory)
register(ConditionToDoFactory)
register(ConditionAdminTaskFactory)
register(ConditionIntegrationsRevokedFactory)
register(PendingAdminTaskFactory)
register(PendingEmailMessageFactory)
register(PendingSlackMessageFactory)
Expand Down
17 changes: 17 additions & 0 deletions back/users/migrations/0038_user_ran_integrations_condition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.5 on 2023-11-21 00:33

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("users", "0037_user_hardware"),
]

operations = [
migrations.AddField(
model_name="user",
name="ran_integrations_condition",
field=models.BooleanField(default=False),
),
]
33 changes: 32 additions & 1 deletion back/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property, lazy
from django.utils.translation import gettext_lazy as _
from django_q.tasks import async_task

from admin.appointments.models import Appointment
from admin.badges.models import Badge
Expand Down Expand Up @@ -196,6 +197,7 @@ class Role(models.IntegerChoices):
blank=True,
help_text=_("Last day of working"),
)
ran_integrations_condition = models.BooleanField(default=False)
unique_url = models.CharField(max_length=250, unique=True)
extra_fields = models.JSONField(default=dict)

Expand Down Expand Up @@ -808,7 +810,6 @@ class NewHireWelcomeMessage(models.Model):


class IntegrationUser(models.Model):
# UserIntegration
# logging when an integration was enabled and revoked
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
integration = models.ForeignKey(
Expand All @@ -819,6 +820,36 @@ class IntegrationUser(models.Model):
class Meta:
unique_together = ["user", "integration"]

def save(self, *args, **kwargs):
integration_user = super().save(*args, **kwargs)
user = self.user
if (
user.is_offboarding
and not user.ran_integrations_condition
and user.conditions.filter(
condition_type=Condition.Type.INTEGRATIONS_REVOKED
).exists()
and not IntegrationUser.objects.filter(user=user, revoked=False).exists()
):
from admin.sequences.tasks import process_condition

integration_revoked_condition = user.conditions.get(
condition_type=Condition.Type.INTEGRATIONS_REVOKED
)
async_task(
process_condition,
integration_revoked_condition.id,
user.id,
task_name=(
f"Process condition: {integration_revoked_condition.id} for "
f"{user.full_name} - all integrations revoked"
),
)
user.ran_integrations_condition = True
user.save()

return integration_user


class OTPRecoveryKey(models.Model):
user = models.ForeignKey(
Expand Down
4 changes: 4 additions & 0 deletions back/users/templates/admin_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,10 @@ <h2 class="page-title">
$("#add-condition-form #div_id_condition_to_do").parent().addClass("d-none")
$("#div_id_condition_admin_tasks").parent().addClass("d-none")
$("#div_id_days").parent().removeClass("d-none")
} else if (option == 5) {
$("#add-condition-form #div_id_condition_to_do").parent().addClass("d-none")
$("#div_id_condition_admin_tasks").parent().addClass("d-none")
$("#div_id_days").parent().addClass("d-none")
} else {
$("#add-condition-form #div_id_condition_to_do").parent().addClass("d-none")
$("#div_id_condition_admin_tasks").parent().removeClass("d-none")
Expand Down
Loading

0 comments on commit 2220b81

Please sign in to comment.