From 915b5c867edcf3cede51742da1707d9110a7e87d Mon Sep 17 00:00:00 2001 From: Jenna Diop Date: Mon, 14 Oct 2024 14:16:02 +0200 Subject: [PATCH 1/5] Get invitation from Slack for special user if it's a P1 --- src/firefighter/incidents/models/group.py | 1 + src/firefighter/incidents/models/incident.py | 4 +- src/firefighter/slack/admin.py | 39 ++++++++++++++++++- .../slack/migrations/0002_usergroup_tag.py | 22 +++++++++++ src/firefighter/slack/models/user_group.py | 6 +++ src/firefighter/slack/signals/get_users.py | 32 +++++++++++++-- 6 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 src/firefighter/slack/migrations/0002_usergroup_tag.py diff --git a/src/firefighter/incidents/models/group.py b/src/firefighter/incidents/models/group.py index 6c1da4c4..6c3b3e38 100644 --- a/src/firefighter/incidents/models/group.py +++ b/src/firefighter/incidents/models/group.py @@ -7,6 +7,7 @@ class Group(models.Model): + """Group of [Components][firefighter.incidents.models.component.Component]. Not a group of users.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=128, unique=True) description = models.TextField(blank=True) diff --git a/src/firefighter/incidents/models/incident.py b/src/firefighter/incidents/models/incident.py index 373e0f64..5c65c036 100644 --- a/src/firefighter/incidents/models/incident.py +++ b/src/firefighter/incidents/models/incident.py @@ -53,7 +53,7 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: - from collections.abc import Sequence # noqa: F401 + from collections.abc import Iterable, Sequence # noqa: F401 from decimal import Decimal from uuid import UUID @@ -460,7 +460,7 @@ def build_invite_list(self) -> list[User]: users_list: list[User] = [] # Send signal to modules (Confluence, PagerDuty...) - result_users: list[tuple[Any, Exception | list[User]]] = ( + result_users: list[tuple[Any, Exception | Iterable[User]]] = ( signals.get_invites.send_robust(sender=None, incident=self) ) diff --git a/src/firefighter/slack/admin.py b/src/firefighter/slack/admin.py index 0b81af12..aa7b7fa5 100644 --- a/src/firefighter/slack/admin.py +++ b/src/firefighter/slack/admin.py @@ -189,8 +189,45 @@ class MessageAdmin(admin.ModelAdmin[Message]): class UserGroupAdmin(admin.ModelAdmin[UserGroup]): model = UserGroup + list_display = [ + "name", + "handle", + "usergroup_id", + "is_external", + "tag", + ] + + list_display_links = [ + "name", + "handle", + "usergroup_id", + ] + + readonly_fields = ( + "created_at", + "updated_at", + ) + autocomplete_fields = ["components", "members"] - search_fields = ["name", "handle", "usergroup_id"] + search_fields = ["name", "handle", "description", "usergroup_id", "tag"] + + fieldsets = ( + ( + ("Slack attributes"), + { + "description" : ("These fields are synchronized automatically with Slack API"), + "fields": ( + "name", + "handle", + "usergroup_id", + "description", + "is_external", + "members", + ) + }, + ), + (_("Firefighter attributes"), {"fields": ("tag", "components", "created_at", "updated_at")}), + ) def save_model( self, diff --git a/src/firefighter/slack/migrations/0002_usergroup_tag.py b/src/firefighter/slack/migrations/0002_usergroup_tag.py new file mode 100644 index 00000000..f95b587c --- /dev/null +++ b/src/firefighter/slack/migrations/0002_usergroup_tag.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.16 on 2024-10-14 10:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("slack", "0001_initial_oss"), + ] + + operations = [ + migrations.AddField( + model_name="usergroup", + name="tag", + field=models.CharField( + blank=True, + help_text="Used by FireFighter internally to mark special users group (@team-secu, @team-incidents ...). Must be empty or unique.", + max_length=80, + ), + ), + ] diff --git a/src/firefighter/slack/models/user_group.py b/src/firefighter/slack/models/user_group.py index 6f8eae68..f3b7f663 100644 --- a/src/firefighter/slack/models/user_group.py +++ b/src/firefighter/slack/models/user_group.py @@ -174,6 +174,12 @@ class UserGroup(models.Model): help_text="Incident created with this usergroup automatically add the group members to these components.", ) + tag = models.CharField( + max_length=80, + blank=True, + help_text="Used by FireFighter internally to mark special users group (@team-secu, @team-incidents ...). Must be empty or unique.", + ) + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/src/firefighter/slack/signals/get_users.py b/src/firefighter/slack/signals/get_users.py index 3f7fdfaa..05efb59c 100644 --- a/src/firefighter/slack/signals/get_users.py +++ b/src/firefighter/slack/signals/get_users.py @@ -8,20 +8,22 @@ from firefighter.incidents import signals from firefighter.incidents.models.user import User +from firefighter.slack.models.user_group import UserGroup from firefighter.slack.slack_app import SlackApp if TYPE_CHECKING: + from collections.abc import Iterable + from django.db.models.query import QuerySet from firefighter.incidents.models.incident import Incident from firefighter.slack.models.conversation import Conversation - from firefighter.slack.models.user_group import UserGroup logger = logging.getLogger(__name__) @receiver(signal=signals.get_invites) -def get_invites_from_slack(incident: Incident, **_kwargs: Any) -> list[User]: +def get_invites_from_slack(incident: Incident, **_kwargs: Any) -> Iterable[User]: """New version using cached users instead of querying Slack API.""" # Prepare sub-queries slack_usergroups: QuerySet[UserGroup] = incident.component.usergroups.all() @@ -39,4 +41,28 @@ def get_invites_from_slack(incident: Incident, **_kwargs: Any) -> list[User]: ) .distinct() ) - return list(queryset) + return set(queryset) + + +@receiver(signal=signals.get_invites) +def get_invites_from_slack_for_p1(incident: Incident, **kwargs: Any) -> Iterable[User]: + + if incident.priority.value > 1: + return [] + + if incident.private: + return [] + + slack_usergroups: QuerySet[UserGroup] = UserGroup.objects.filter( + tag="invited_for_all_public_p1" + ) + + queryset = ( + User.objects.filter(slack_user__isnull=False) + .exclude(slack_user__slack_id=SlackApp().details["user_id"]) + .exclude(slack_user__slack_id="") + .exclude(slack_user__slack_id__isnull=True) + .filter(usergroup__in=slack_usergroups) + .distinct() + ) + return set(queryset) From e925570c1280afe585f65495f16772513912679d Mon Sep 17 00:00:00 2001 From: Jenna Diop Date: Tue, 15 Oct 2024 12:41:41 +0200 Subject: [PATCH 2/5] add docs to explain how we invite users to incidents --- docs/usage/integrations.md | 49 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/usage/integrations.md b/docs/usage/integrations.md index 82f06d22..29da2799 100644 --- a/docs/usage/integrations.md +++ b/docs/usage/integrations.md @@ -67,13 +67,58 @@ Some tags have special meaning: - `dev_firefighter`: Where users can get help with the bot. Will be shown in `/incident help` for instance. - `it_deploy`: Where the bot send notifications for deployment freezes. -##### Usergroups +## User Group Management in Back-Office -You can add or import usergroups in the back-office. +You can **add** or **import user groups** in the back-office. !!! note "Hint" When adding a usergroup in the BackOffice, you can put only its ID. The rest of the information will be fetched from Slack. +### How users are invited into an incident + +#### Signal Receiver for Invitations + +1. **Signal Receiver**: + - We utilize a signal receiver from the function `get_invites_from_pagerduty`, which listens for the `get_invites` signal. + +2. **Adding Specific User Groups**: + - In our example, we have set up listeners to add a specific user group when an incident is classified as a P1. + +#### Handling Invitations from Slack + +- The function `get_invites_from_slack` also listens for the `get_invites` signal. + +- **Logic**: + - We first check if the incident is a P1 or if it is private. + - Then, we filter user groups tagged as `"invited_for_all_public_p1"`. + - The function retrieves users from these groups who have a linked Slack user account, excludes the bot user, and returns a distinct set of users to be invited. + +#### Aggregation of Responders + +- The `build_invite_list` method in the `Incident` model sends the signal to `get_invites` to gather users from all integrations. + +- **User List Aggregation**: + - This results in a comprehensive list that aggregates users from all providers. + + + ##### SOSes You can configure [SOSes][firefighter.slack.models.sos.Sos] in the back-office. From 57582d8f102f0d4723085f38de4eeef7f305c3b3 Mon Sep 17 00:00:00 2001 From: Jenna Diop Date: Wed, 16 Oct 2024 09:39:28 +0200 Subject: [PATCH 3/5] docs review feedback --- docs/usage/integrations.md | 43 +++++++++----------------------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/docs/usage/integrations.md b/docs/usage/integrations.md index 29da2799..a4040ab5 100644 --- a/docs/usage/integrations.md +++ b/docs/usage/integrations.md @@ -76,48 +76,25 @@ You can **add** or **import user groups** in the back-office. ### How users are invited into an incident -#### Signal Receiver for Invitations +Users are invited to incidents through a system that listens for invitation requests. For critical incidents, specific user groups are automatically included in the invitation process. -1. **Signal Receiver**: - - We utilize a signal receiver from the function `get_invites_from_pagerduty`, which listens for the `get_invites` signal. +The system also checks if the incident is public or private, ensuring that only the appropriate users with Slack accounts are invited. This creates a complete list of responders from all connected platforms, making sure the right people are notified. -2. **Adding Specific User Groups**: - - In our example, we have set up listeners to add a specific user group when an incident is classified as a P1. +### Custom Invitation Strategy -#### Handling Invitations from Slack +For users looking to create a custom invitation strategy, here’s what you need to know: -- The function `get_invites_from_slack` also listens for the `get_invites` signal. +- **Django Signals**: We use Django signals to manage invitations. You can refer to the [Django signals documentation](https://docs.djangoproject.com/en/4.2/topics/signals/) for more information. -- **Logic**: - - We first check if the incident is a P1 or if it is private. - - Then, we filter user groups tagged as `"invited_for_all_public_p1"`. - - The function retrieves users from these groups who have a linked Slack user account, excludes the bot user, and returns a distinct set of users to be invited. -#### Aggregation of Responders +- **Registering on the Signal**: You need to register on the `get_invites signal`, which provides the incident object and expects to receive a list of users. -- The `build_invite_list` method in the `Incident` model sends the signal to `get_invites` to gather users from all integrations. +- **Signal Example**: You can check one of our [signals][firefighter.slack.signals.get_users] for a concrete example. -- **User List Aggregation**: - - This results in a comprehensive list that aggregates users from all providers. +**Tips**: + The signal can be triggered during the creation and update of an incident. - + Invitations will only be sent once all signals have responded. It is advisable to avoid API calls and to store data in the database beforehand. ##### SOSes From 66699e72e37751359489ca517f11d9fc1201082c Mon Sep 17 00:00:00 2001 From: Jenna Diop Date: Wed, 16 Oct 2024 09:55:32 +0200 Subject: [PATCH 4/5] feedback docs typo --- docs/usage/integrations.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/usage/integrations.md b/docs/usage/integrations.md index a4040ab5..c053deee 100644 --- a/docs/usage/integrations.md +++ b/docs/usage/integrations.md @@ -87,14 +87,13 @@ For users looking to create a custom invitation strategy, here’s what you need - **Django Signals**: We use Django signals to manage invitations. You can refer to the [Django signals documentation](https://docs.djangoproject.com/en/4.2/topics/signals/) for more information. -- **Registering on the Signal**: You need to register on the `get_invites signal`, which provides the incident object and expects to receive a list of users. +- **Registering on the Signal**: You need to register on the [`get_invites`][firefighter.incidents.signals.get_invites] signal, which provides the incident object and expects to receive a list of [`users`][firefighter.slack.models.user]. - **Signal Example**: You can check one of our [signals][firefighter.slack.signals.get_users] for a concrete example. -**Tips**: - The signal can be triggered during the creation and update of an incident. - - Invitations will only be sent once all signals have responded. It is advisable to avoid API calls and to store data in the database beforehand. +!!! note "Tips" + The signal can be triggered during the creation and update of an incident. + Invitations will only be sent once all signals have responded. It is advisable to avoid API calls and to store data in the database beforehand. ##### SOSes From 6c7c979fce0f1e47073669ad9ac512775a3b03a9 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Wed, 16 Oct 2024 09:57:17 +0200 Subject: [PATCH 5/5] Update src/firefighter/slack/models/user_group.py --- src/firefighter/slack/models/user_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firefighter/slack/models/user_group.py b/src/firefighter/slack/models/user_group.py index f3b7f663..c0e54aef 100644 --- a/src/firefighter/slack/models/user_group.py +++ b/src/firefighter/slack/models/user_group.py @@ -177,7 +177,7 @@ class UserGroup(models.Model): tag = models.CharField( max_length=80, blank=True, - help_text="Used by FireFighter internally to mark special users group (@team-secu, @team-incidents ...). Must be empty or unique.", + help_text="Used by FireFighter internally to mark special user groups (e.g. @team-secu, @team-incidents...). Must be empty or unique.", ) created_at = models.DateTimeField(auto_now_add=True)