✨(teams) add matrix webhook for teams
A webhook to invite/kick team members to a matrix room.
This commit is contained in:
committed by
Quentin BEY
parent
7bebf13d88
commit
cc39ed5298
@@ -10,6 +10,7 @@ and this project adheres to
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- ✨(teams) add matrix webhook for teams #904
|
||||||
- ✨(resource-server) add SCIM /Me endpoint #895
|
- ✨(resource-server) add SCIM /Me endpoint #895
|
||||||
- 🔧(git) set LF line endings for all text files #928
|
- 🔧(git) set LF line endings for all text files #928
|
||||||
|
|
||||||
|
|||||||
@@ -24,3 +24,10 @@ class WebhookStatusChoices(models.TextChoices):
|
|||||||
FAILURE = "failure", _("Failure")
|
FAILURE = "failure", _("Failure")
|
||||||
PENDING = "pending", _("Pending")
|
PENDING = "pending", _("Pending")
|
||||||
SUCCESS = "success", _("Success")
|
SUCCESS = "success", _("Success")
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookProtocolChoices(models.TextChoices):
|
||||||
|
"""Defines the possible protocols of webhook."""
|
||||||
|
|
||||||
|
SCIM = "scim"
|
||||||
|
MATRIX = "matrix"
|
||||||
|
|||||||
18
src/backend/core/migrations/0017_teamwebhook_protocol.py
Normal file
18
src/backend/core/migrations/0017_teamwebhook_protocol.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.3 on 2025-06-17 14:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0016_team_external_id_alter_team_users'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='teamwebhook',
|
||||||
|
name='protocol',
|
||||||
|
field=models.CharField(choices=[('scim', 'Scim'), ('matrix', 'Matrix')], default='scim'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -21,7 +21,7 @@ from django.contrib.postgres.fields import ArrayField
|
|||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.core import exceptions, mail, validators
|
from django.core import exceptions, mail, validators
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models, transaction
|
from django.db import models
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext, override
|
from django.utils.translation import gettext, override
|
||||||
@@ -31,9 +31,9 @@ import jsonschema
|
|||||||
from timezone_field import TimeZoneField
|
from timezone_field import TimeZoneField
|
||||||
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
||||||
|
|
||||||
from core.enums import WebhookStatusChoices
|
from core.enums import WebhookProtocolChoices, WebhookStatusChoices
|
||||||
from core.plugins.registry import registry as plugin_hooks_registry
|
from core.plugins.registry import registry as plugin_hooks_registry
|
||||||
from core.utils.webhooks import scim_synchronizer
|
from core.utils.webhooks import webhooks_synchronizer
|
||||||
from core.validators import get_field_validators_from_setting
|
from core.validators import get_field_validators_from_setting
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
@@ -864,16 +864,12 @@ class TeamAccess(BaseModel):
|
|||||||
Override save function to fire webhooks on any addition or update
|
Override save function to fire webhooks on any addition or update
|
||||||
to a team access.
|
to a team access.
|
||||||
"""
|
"""
|
||||||
|
if self._state.adding and self.team.webhooks.exists():
|
||||||
if self._state.adding:
|
|
||||||
self.team.webhooks.update(status=WebhookStatusChoices.PENDING)
|
self.team.webhooks.update(status=WebhookStatusChoices.PENDING)
|
||||||
with transaction.atomic():
|
# try to synchronize all webhooks
|
||||||
instance = super().save(*args, **kwargs)
|
webhooks_synchronizer.add_user_to_group(self.team, self.user)
|
||||||
scim_synchronizer.add_user_to_group(self.team, self.user)
|
|
||||||
else:
|
|
||||||
instance = super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
return instance
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -881,11 +877,12 @@ class TeamAccess(BaseModel):
|
|||||||
Don't allow deleting a team access until it is successfully synchronized with all
|
Don't allow deleting a team access until it is successfully synchronized with all
|
||||||
its webhooks.
|
its webhooks.
|
||||||
"""
|
"""
|
||||||
self.team.webhooks.update(status=WebhookStatusChoices.PENDING)
|
if webhooks := self.team.webhooks.all():
|
||||||
with transaction.atomic():
|
webhooks.update(status=WebhookStatusChoices.PENDING)
|
||||||
arguments = self.team, self.user
|
# try to synchronize all webhooks
|
||||||
super().delete(*args, **kwargs)
|
webhooks_synchronizer.remove_user_from_group(self.team, self.user)
|
||||||
scim_synchronizer.remove_user_from_group(*arguments)
|
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
def get_abilities(self, user):
|
def get_abilities(self, user):
|
||||||
"""
|
"""
|
||||||
@@ -943,6 +940,11 @@ class TeamWebhook(BaseModel):
|
|||||||
team = models.ForeignKey(Team, related_name="webhooks", on_delete=models.CASCADE)
|
team = models.ForeignKey(Team, related_name="webhooks", on_delete=models.CASCADE)
|
||||||
url = models.URLField(_("url"))
|
url = models.URLField(_("url"))
|
||||||
secret = models.CharField(_("secret"), max_length=255, null=True, blank=True)
|
secret = models.CharField(_("secret"), max_length=255, null=True, blank=True)
|
||||||
|
protocol = models.CharField(
|
||||||
|
max_length=None,
|
||||||
|
default=WebhookProtocolChoices.SCIM,
|
||||||
|
choices=WebhookProtocolChoices.choices,
|
||||||
|
)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
default=WebhookStatusChoices.PENDING,
|
default=WebhookStatusChoices.PENDING,
|
||||||
|
|||||||
1
src/backend/core/tests/fixtures/__init__.py
vendored
Normal file
1
src/backend/core/tests/fixtures/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Test fixtures."""
|
||||||
75
src/backend/core/tests/fixtures/matrix.py
vendored
Normal file
75
src/backend/core/tests/fixtures/matrix.py
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"""Define here some fake responses from Matrix API, useful to mock responses in tests."""
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
|
||||||
|
# JOIN ROOMS
|
||||||
|
def mock_join_room_successful(room_id):
|
||||||
|
"""Mock response when succesfully joining room. Same response if already in room."""
|
||||||
|
return {"message": {"room_id": str(room_id)}, "status_code": status.HTTP_200_OK}
|
||||||
|
|
||||||
|
|
||||||
|
def mock_join_room_no_known_servers():
|
||||||
|
"""Mock response when room to join cannot be found."""
|
||||||
|
return {
|
||||||
|
"message": {"errcode": "M_UNKNOWN", "error": "No known servers"},
|
||||||
|
"status_code": status.HTTP_404_NOT_FOUND,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mock_join_room_forbidden():
|
||||||
|
"""Mock response when room cannot be joined."""
|
||||||
|
return {
|
||||||
|
"message": {
|
||||||
|
"errcode": "M_FORBIDDEN",
|
||||||
|
"error": "You do not belong to any of the required rooms/spaces to join this room.",
|
||||||
|
},
|
||||||
|
"status_code": status.HTTP_403_FORBIDDEN,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# INVITE USER
|
||||||
|
def mock_invite_successful():
|
||||||
|
"""Mock response when invite request was succesful. Does not check the user exists."""
|
||||||
|
return {"message": {}, "status_code": status.HTTP_200_OK}
|
||||||
|
|
||||||
|
|
||||||
|
def mock_invite_user_already_in_room(user):
|
||||||
|
"""Mock response when invitation forbidden for People user."""
|
||||||
|
return {
|
||||||
|
"message": {
|
||||||
|
"errcode": "M_FORBIDDEN",
|
||||||
|
"error": f"{user.email.replace('@', ':')} is already in the room.",
|
||||||
|
},
|
||||||
|
"status_code": status.HTTP_403_FORBIDDEN,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# KICK USER
|
||||||
|
def mock_kick_successful():
|
||||||
|
"""Mock response when succesfully joining room."""
|
||||||
|
return {"message": {}, "status_code": status.HTTP_200_OK}
|
||||||
|
|
||||||
|
|
||||||
|
def mock_kick_user_forbidden(user):
|
||||||
|
"""Mock response when kick request is forbidden (i.e. wrong permission or user is room admin."""
|
||||||
|
return {
|
||||||
|
"message": {
|
||||||
|
"errcode": "M_FORBIDDEN",
|
||||||
|
"error": f"You cannot kick user @{user.email.replace('@', ':')}.",
|
||||||
|
},
|
||||||
|
"status_code": status.HTTP_403_FORBIDDEN,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mock_kick_user_not_in_room():
|
||||||
|
"""
|
||||||
|
Mock response when trying to kick a user who isn't in the room. Don't check the user exists.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"message": {
|
||||||
|
"errcode": "M_FORBIDDEN",
|
||||||
|
"error": "The target user is not in the room",
|
||||||
|
},
|
||||||
|
"status_code": status.HTTP_403_FORBIDDEN,
|
||||||
|
}
|
||||||
@@ -3,14 +3,17 @@ Test for team accesses API endpoints in People's core app : create
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import responses
|
import responses
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from core import factories, models
|
from core import enums, factories, models
|
||||||
|
from core.tests.fixtures import matrix
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
@@ -171,14 +174,17 @@ def test_api_team_accesses_create_authenticated_owner():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_team_accesses_create_webhook():
|
def test_api_team_accesses_create__with_scim_webhook():
|
||||||
"""
|
"""
|
||||||
When the team has a webhook, creating a team access should fire a call.
|
If a team has a SCIM webhook, creating a team access should fire a call
|
||||||
|
with the expected payload.
|
||||||
"""
|
"""
|
||||||
user, other_user = factories.UserFactory.create_batch(2)
|
user, other_user = factories.UserFactory.create_batch(2)
|
||||||
|
|
||||||
team = factories.TeamFactory(users=[(user, "owner")])
|
team = factories.TeamFactory(users=[(user, "owner")])
|
||||||
webhook = factories.TeamWebhookFactory(team=team)
|
webhook = factories.TeamWebhookFactory(
|
||||||
|
team=team, protocol=enums.WebhookProtocolChoices.SCIM
|
||||||
|
)
|
||||||
|
|
||||||
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
||||||
|
|
||||||
@@ -226,3 +232,139 @@ def test_api_team_accesses_create_webhook():
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert models.TeamAccess.objects.filter(user=other_user, team=team).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_team_accesses_create__multiple_webhooks_success(caplog):
|
||||||
|
"""
|
||||||
|
When the team has multiple webhooks, creating a team access should fire all the expected calls.
|
||||||
|
If all responses are positive, proceeds to add the user to the team.
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
user, other_user = factories.UserFactory.create_batch(2)
|
||||||
|
|
||||||
|
team = factories.TeamFactory(users=[(user, "owner")])
|
||||||
|
webhook_scim = factories.TeamWebhookFactory(
|
||||||
|
team=team, protocol=enums.WebhookProtocolChoices.SCIM, secret="wesh"
|
||||||
|
)
|
||||||
|
webhook_matrix = factories.TeamWebhookFactory(
|
||||||
|
team=team,
|
||||||
|
url="https://www.webhookserver.fr/#/room/room_id:home_server/",
|
||||||
|
protocol=enums.WebhookProtocolChoices.MATRIX,
|
||||||
|
secret="yo",
|
||||||
|
)
|
||||||
|
|
||||||
|
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
with responses.RequestsMock() as rsps:
|
||||||
|
# Ensure successful response by scim provider using "responses":
|
||||||
|
rsps.add(
|
||||||
|
rsps.PATCH,
|
||||||
|
re.compile(r".*/Groups/.*"),
|
||||||
|
body="{}",
|
||||||
|
status=200,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
rsps.add(
|
||||||
|
rsps.POST,
|
||||||
|
re.compile(r".*/join"),
|
||||||
|
body=str(matrix.mock_join_room_successful),
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
rsps.add(
|
||||||
|
rsps.POST,
|
||||||
|
re.compile(r".*/invite"),
|
||||||
|
body=str(matrix.mock_invite_successful()["message"]),
|
||||||
|
status=matrix.mock_invite_successful()["status_code"],
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||||
|
{
|
||||||
|
"user": str(other_user.id),
|
||||||
|
"role": role,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
# Logger
|
||||||
|
log_messages = [msg.message for msg in caplog.records]
|
||||||
|
for webhook in [webhook_scim, webhook_matrix]:
|
||||||
|
assert (
|
||||||
|
f"add_user_to_group synchronization succeeded with {webhook.url}"
|
||||||
|
in log_messages
|
||||||
|
)
|
||||||
|
|
||||||
|
# Status
|
||||||
|
for webhook in [webhook_scim, webhook_matrix]:
|
||||||
|
webhook.refresh_from_db()
|
||||||
|
assert webhook.status == "success"
|
||||||
|
assert models.TeamAccess.objects.filter(user=other_user, team=team).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_api_team_accesses_create__multiple_webhooks_failure(caplog):
|
||||||
|
"""When a webhook fails, user should still be added to the team."""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
user, other_user = factories.UserFactory.create_batch(2)
|
||||||
|
|
||||||
|
team = factories.TeamFactory(users=[(user, "owner")])
|
||||||
|
webhook_scim = factories.TeamWebhookFactory(
|
||||||
|
team=team, protocol=enums.WebhookProtocolChoices.SCIM, secret="wesh"
|
||||||
|
)
|
||||||
|
webhook_matrix = factories.TeamWebhookFactory(
|
||||||
|
team=team,
|
||||||
|
url="https://www.webhookserver.fr/#/room/room_id:home_server/",
|
||||||
|
protocol=enums.WebhookProtocolChoices.MATRIX,
|
||||||
|
secret="secret",
|
||||||
|
)
|
||||||
|
|
||||||
|
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
responses.patch(
|
||||||
|
re.compile(r".*/Groups/.*"),
|
||||||
|
body="{}",
|
||||||
|
status=200,
|
||||||
|
)
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/join"),
|
||||||
|
body=str(matrix.mock_join_room_forbidden()["message"]),
|
||||||
|
status=str(matrix.mock_join_room_forbidden()["status_code"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||||
|
{
|
||||||
|
"user": str(other_user.id),
|
||||||
|
"role": role,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_201_CREATED
|
||||||
|
|
||||||
|
# Logger
|
||||||
|
log_messages = [msg.message for msg in caplog.records]
|
||||||
|
assert (
|
||||||
|
f"add_user_to_group synchronization succeeded with {webhook_scim.url}"
|
||||||
|
in log_messages
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
f"add_user_to_group synchronization failed with {webhook_matrix.url}"
|
||||||
|
in log_messages
|
||||||
|
)
|
||||||
|
|
||||||
|
# Status
|
||||||
|
webhook_scim.status = "success"
|
||||||
|
webhook_matrix.status = "failure"
|
||||||
|
assert models.TeamAccess.objects.filter(user=other_user, team=team).exists()
|
||||||
|
|||||||
277
src/backend/core/tests/utils/test_webhooks_matrix_client.py
Normal file
277
src/backend/core/tests/utils/test_webhooks_matrix_client.py
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
"""Test Team synchronization webhooks : focus on matrix client."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import responses
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from core import factories
|
||||||
|
from core.enums import WebhookProtocolChoices
|
||||||
|
from core.tests.fixtures import matrix
|
||||||
|
from core.utils.webhooks import webhooks_synchronizer
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
## INVITE
|
||||||
|
@responses.activate
|
||||||
|
def test_matrix_webhook__invite_user_to_room_forbidden(caplog):
|
||||||
|
"""Cannot invite when Matrix returns forbidden. This might mean the user is an admin."""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
webhook = factories.TeamWebhookFactory(
|
||||||
|
protocol=WebhookProtocolChoices.MATRIX,
|
||||||
|
url="https://www.matrix.org/#/room/room_id:home_server",
|
||||||
|
secret="secret-access-token",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock successful responses
|
||||||
|
error = matrix.mock_kick_user_forbidden(user)
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/join"),
|
||||||
|
body=str(matrix.mock_join_room_successful),
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/invite"),
|
||||||
|
body=str(error["message"]),
|
||||||
|
status=error["status_code"],
|
||||||
|
)
|
||||||
|
webhooks_synchronizer.add_user_to_group(team=webhook.team, user=user)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_matrix_webhook__invite_user_to_room_already_in_room(caplog):
|
||||||
|
"""If user is already in room, webhooks should be set to success."""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
webhook = factories.TeamWebhookFactory(
|
||||||
|
protocol=WebhookProtocolChoices.MATRIX,
|
||||||
|
url="https://www.matrix.org/#/room/room_id:home_server",
|
||||||
|
secret="secret-access-token",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock successful responses
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/join"),
|
||||||
|
body=str(matrix.mock_join_room_successful("room_id")["message"]),
|
||||||
|
status=matrix.mock_join_room_successful("room_id")["status_code"],
|
||||||
|
)
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/invite"),
|
||||||
|
body=str(matrix.mock_invite_user_already_in_room(user)["message"]),
|
||||||
|
status=matrix.mock_invite_user_already_in_room(user)["status_code"],
|
||||||
|
)
|
||||||
|
webhooks_synchronizer.add_user_to_group(team=webhook.team, user=user)
|
||||||
|
|
||||||
|
# Logger
|
||||||
|
log_messages = [msg.message for msg in caplog.records]
|
||||||
|
expected_messages = (
|
||||||
|
f"add_user_to_group synchronization succeeded with {webhook.url}"
|
||||||
|
)
|
||||||
|
assert expected_messages in log_messages
|
||||||
|
|
||||||
|
# Status
|
||||||
|
webhook.refresh_from_db()
|
||||||
|
assert webhook.status == "success"
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_matrix_webhook__invite_user_to_room_success(caplog):
|
||||||
|
"""The user passed to the function should get invited."""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
webhook = factories.TeamWebhookFactory(
|
||||||
|
protocol=WebhookProtocolChoices.MATRIX,
|
||||||
|
url="https://www.matrix.org/#/room/room_id:home_server",
|
||||||
|
secret="secret-access-token",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock successful responses
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/join"),
|
||||||
|
body=str(matrix.mock_join_room_successful("room_id")["message"]),
|
||||||
|
status=matrix.mock_join_room_successful("room_id")["status_code"],
|
||||||
|
)
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/invite"),
|
||||||
|
body=str(matrix.mock_invite_successful()["message"]),
|
||||||
|
status=matrix.mock_invite_successful()["status_code"],
|
||||||
|
)
|
||||||
|
webhooks_synchronizer.add_user_to_group(team=webhook.team, user=user)
|
||||||
|
|
||||||
|
# Check headers
|
||||||
|
headers = responses.calls[0].request.headers
|
||||||
|
assert webhook.secret in headers["Authorization"]
|
||||||
|
|
||||||
|
# Check payloads sent to Matrix API
|
||||||
|
assert json.loads(responses.calls[1].request.body) == {
|
||||||
|
"user_id": f"@{user.email.replace('@', ':')}",
|
||||||
|
"reason": f"User added to team {webhook.team} on People",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logger
|
||||||
|
log_messages = [msg.message for msg in caplog.records]
|
||||||
|
expected_messages = (
|
||||||
|
f"add_user_to_group synchronization succeeded with {webhook.url}"
|
||||||
|
)
|
||||||
|
assert expected_messages in log_messages
|
||||||
|
|
||||||
|
# Status
|
||||||
|
webhook.refresh_from_db()
|
||||||
|
assert webhook.status == "success"
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
@override_settings(TCHAP_ACCESS_TOKEN="TCHAP_TOKEN")
|
||||||
|
def test_matrix_webhook__override_secret_for_tchap():
|
||||||
|
"""The user passed to the function should get invited."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
webhook = factories.TeamWebhookFactory(
|
||||||
|
protocol=WebhookProtocolChoices.MATRIX,
|
||||||
|
url="https://www.tchap.gouv.fr/#/room/room_id:home_server",
|
||||||
|
secret="secret-about-to-be-overridden",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock successful responses
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/join"),
|
||||||
|
body=str(matrix.mock_join_room_successful("room_id")["message"]),
|
||||||
|
status=matrix.mock_join_room_successful("room_id")["status_code"],
|
||||||
|
)
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/invite"),
|
||||||
|
body=str(matrix.mock_invite_successful()["message"]),
|
||||||
|
status=matrix.mock_invite_successful()["status_code"],
|
||||||
|
)
|
||||||
|
webhooks_synchronizer.add_user_to_group(team=webhook.team, user=user)
|
||||||
|
|
||||||
|
# Check headers
|
||||||
|
headers = responses.calls[0].request.headers
|
||||||
|
assert "TCHAP_TOKEN" in headers["Authorization"]
|
||||||
|
|
||||||
|
|
||||||
|
## KICK
|
||||||
|
@responses.activate
|
||||||
|
def test_matrix_webhook__kick_user_from_room_not_in_room(caplog):
|
||||||
|
"""Webhook should report a success when user was already not in room."""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
webhook = factories.TeamWebhookFactory(
|
||||||
|
protocol=WebhookProtocolChoices.MATRIX,
|
||||||
|
url="https://www.matrix.org/#/room/room_id:home_server",
|
||||||
|
secret="secret-access-token",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock successful responses
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/join"),
|
||||||
|
body=str(matrix.mock_join_room_successful),
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/kick"),
|
||||||
|
body=str(matrix.mock_kick_user_not_in_room()["message"]),
|
||||||
|
status=matrix.mock_kick_user_not_in_room()["status_code"],
|
||||||
|
)
|
||||||
|
webhooks_synchronizer.remove_user_from_group(team=webhook.team, user=user)
|
||||||
|
|
||||||
|
# Logger
|
||||||
|
log_messages = [msg.message for msg in caplog.records]
|
||||||
|
assert (
|
||||||
|
f"remove_user_from_group synchronization succeeded with {webhook.url}"
|
||||||
|
in log_messages
|
||||||
|
)
|
||||||
|
|
||||||
|
# Status
|
||||||
|
webhook.refresh_from_db()
|
||||||
|
assert webhook.status == "success"
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_matrix_webhook__kick_user_from_room_success(caplog):
|
||||||
|
"""The user passed to the function should get removed."""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
webhook = factories.TeamWebhookFactory(
|
||||||
|
protocol=WebhookProtocolChoices.MATRIX,
|
||||||
|
url="https://www.matrix.org/#/room/room_id:home_server",
|
||||||
|
secret="secret-access-token",
|
||||||
|
)
|
||||||
|
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/join"),
|
||||||
|
body=str(matrix.mock_join_room_successful),
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/kick"),
|
||||||
|
body=str(matrix.mock_kick_successful),
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
webhooks_synchronizer.remove_user_from_group(team=webhook.team, user=user)
|
||||||
|
|
||||||
|
# Check payloads sent to Matrix API
|
||||||
|
assert json.loads(responses.calls[1].request.body) == {
|
||||||
|
"user_id": f"@{user.email.replace('@', ':')}",
|
||||||
|
"reason": f"User removed from team {webhook.team} on People",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logger
|
||||||
|
log_messages = [msg.message for msg in caplog.records]
|
||||||
|
expected_messages = (
|
||||||
|
f"remove_user_from_group synchronization succeeded with {webhook.url}"
|
||||||
|
)
|
||||||
|
assert expected_messages in log_messages
|
||||||
|
|
||||||
|
# Status
|
||||||
|
webhook.refresh_from_db()
|
||||||
|
assert webhook.status == "success"
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_matrix_webhook__kick_user_from_room_forbidden(caplog):
|
||||||
|
"""Cannot kick an admin."""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
webhook = factories.TeamWebhookFactory(
|
||||||
|
protocol=WebhookProtocolChoices.MATRIX,
|
||||||
|
url="https://www.matrix.org/#/room/room_id:home_server",
|
||||||
|
secret="secret-access-token",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock successful responses
|
||||||
|
error = matrix.mock_kick_user_forbidden(user)
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/join"),
|
||||||
|
body=str(matrix.mock_join_room_successful),
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
responses.post(
|
||||||
|
re.compile(r".*/kick"),
|
||||||
|
body=str(error["message"]),
|
||||||
|
status=error["status_code"],
|
||||||
|
)
|
||||||
|
webhooks_synchronizer.remove_user_from_group(team=webhook.team, user=user)
|
||||||
|
|
||||||
|
# Logger
|
||||||
|
log_messages = [msg.message for msg in caplog.records]
|
||||||
|
assert (
|
||||||
|
f"remove_user_from_group synchronization failed with {webhook.url}"
|
||||||
|
in log_messages
|
||||||
|
)
|
||||||
|
|
||||||
|
# Status
|
||||||
|
webhook.refresh_from_db()
|
||||||
|
assert webhook.status == "failure"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Test Team synchronization webhooks."""
|
"""Test Team synchronization webhooks : focus on scim client"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
@@ -10,7 +10,7 @@ import pytest
|
|||||||
import responses
|
import responses
|
||||||
|
|
||||||
from core import factories
|
from core import factories
|
||||||
from core.utils.webhooks import scim_synchronizer
|
from core.utils.webhooks import webhooks_synchronizer
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ def test_utils_webhooks_add_user_to_group_no_webhooks():
|
|||||||
access = factories.TeamAccessFactory()
|
access = factories.TeamAccessFactory()
|
||||||
|
|
||||||
with responses.RequestsMock():
|
with responses.RequestsMock():
|
||||||
scim_synchronizer.add_user_to_group(access.team, access.user)
|
webhooks_synchronizer.add_user_to_group(access.team, access.user)
|
||||||
|
|
||||||
assert len(responses.calls) == 0
|
assert len(responses.calls) == 0
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ def test_utils_webhooks_add_user_to_group_success(mock_info):
|
|||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
scim_synchronizer.add_user_to_group(access.team, access.user)
|
webhooks_synchronizer.add_user_to_group(access.team, access.user)
|
||||||
|
|
||||||
for i, webhook in enumerate(webhooks):
|
for i, webhook in enumerate(webhooks):
|
||||||
assert rsps.calls[i].request.url == webhook.url
|
assert rsps.calls[i].request.url == webhook.url
|
||||||
@@ -107,7 +107,7 @@ def test_utils_webhooks_remove_user_from_group_success(mock_info):
|
|||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
scim_synchronizer.remove_user_from_group(access.team, access.user)
|
webhooks_synchronizer.remove_user_from_group(access.team, access.user)
|
||||||
|
|
||||||
for i, webhook in enumerate(webhooks):
|
for i, webhook in enumerate(webhooks):
|
||||||
assert rsps.calls[i].request.url == webhook.url
|
assert rsps.calls[i].request.url == webhook.url
|
||||||
@@ -163,11 +163,11 @@ def test_utils_webhooks_add_user_to_group_failure(mock_error):
|
|||||||
rsps.PATCH,
|
rsps.PATCH,
|
||||||
re.compile(r".*/Groups/.*"),
|
re.compile(r".*/Groups/.*"),
|
||||||
body="{}",
|
body="{}",
|
||||||
status=random.choice([404, 301, 302]),
|
status=404,
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
scim_synchronizer.add_user_to_group(access.team, access.user)
|
webhooks_synchronizer.add_user_to_group(access.team, access.user)
|
||||||
|
|
||||||
for i, webhook in enumerate(webhooks):
|
for i, webhook in enumerate(webhooks):
|
||||||
assert rsps.calls[i].request.url == webhook.url
|
assert rsps.calls[i].request.url == webhook.url
|
||||||
@@ -228,7 +228,7 @@ def test_utils_webhooks_add_user_to_group_retries(mock_info, mock_error):
|
|||||||
rsps.add(rsps.PATCH, url, status=200, content_type="application/json"),
|
rsps.add(rsps.PATCH, url, status=200, content_type="application/json"),
|
||||||
]
|
]
|
||||||
|
|
||||||
scim_synchronizer.add_user_to_group(access.team, access.user)
|
webhooks_synchronizer.add_user_to_group(access.team, access.user)
|
||||||
|
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
assert all_rsps[i].call_count == 1
|
assert all_rsps[i].call_count == 1
|
||||||
@@ -285,7 +285,7 @@ def test_utils_synchronize_course_runs_max_retries_exceeded(mock_error):
|
|||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
scim_synchronizer.add_user_to_group(access.team, access.user)
|
webhooks_synchronizer.add_user_to_group(access.team, access.user)
|
||||||
|
|
||||||
assert rsp.call_count == 5
|
assert rsp.call_count == 5
|
||||||
assert rsps.calls[0].request.url == webhook.url
|
assert rsps.calls[0].request.url == webhook.url
|
||||||
@@ -339,7 +339,7 @@ def test_utils_webhooks_add_user_to_group_authorization():
|
|||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
scim_synchronizer.add_user_to_group(access.team, access.user)
|
webhooks_synchronizer.add_user_to_group(access.team, access.user)
|
||||||
assert rsps.calls[0].request.url == webhook.url
|
assert rsps.calls[0].request.url == webhook.url
|
||||||
|
|
||||||
# Check headers
|
# Check headers
|
||||||
|
|||||||
136
src/backend/core/utils/matrix.py
Normal file
136
src/backend/core/utils/matrix.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
"""Matrix client for interoperability to synchronize with remote service providers."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from rest_framework.status import (
|
||||||
|
HTTP_200_OK,
|
||||||
|
)
|
||||||
|
from urllib3.util import Retry
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
adapter = requests.adapters.HTTPAdapter(
|
||||||
|
max_retries=Retry(
|
||||||
|
total=4,
|
||||||
|
backoff_factor=0.1,
|
||||||
|
status_forcelist=[500, 502],
|
||||||
|
allowed_methods=["POST"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
session = requests.Session()
|
||||||
|
session.mount("http://", adapter)
|
||||||
|
session.mount("https://", adapter)
|
||||||
|
|
||||||
|
|
||||||
|
class MatrixAPIClient:
|
||||||
|
"""A client to interact with Matrix API"""
|
||||||
|
|
||||||
|
secret = settings.TCHAP_ACCESS_TOKEN
|
||||||
|
|
||||||
|
def get_headers(self, webhook):
|
||||||
|
"""Build header dict from webhook object."""
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
token = webhook.secret if webhook.secret else None
|
||||||
|
if "tchap.gouv.fr" in webhook.url:
|
||||||
|
token = settings.TCHAP_ACCESS_TOKEN
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def _get_room_url(self, webhook_url):
|
||||||
|
"""Returns room id from webhook url."""
|
||||||
|
room_id = webhook_url.split("/room/")[1]
|
||||||
|
base_url = room_id.split(":")[1]
|
||||||
|
if "tchap.gouv.fr" in webhook_url:
|
||||||
|
base_url = f"matrix.{base_url}"
|
||||||
|
return f"https://{base_url}/_matrix/client/v3/rooms/{room_id}"
|
||||||
|
|
||||||
|
def get_user_id(self, user):
|
||||||
|
"""Returns user id from email."""
|
||||||
|
if user.email is None:
|
||||||
|
raise ValueError("You must first set an email for the user.")
|
||||||
|
|
||||||
|
return f"@{user.email.replace('@', ':')}"
|
||||||
|
|
||||||
|
def join_room(self, webhook):
|
||||||
|
"""Accept invitation to the room. As of today, it is a mandatory step
|
||||||
|
to make sure our account will be able to invite/remove users."""
|
||||||
|
if webhook.secret is None:
|
||||||
|
raise ValueError("Please configure this webhook's secret access token.")
|
||||||
|
|
||||||
|
return session.post(
|
||||||
|
f"{self._get_room_url(webhook.url)}/join",
|
||||||
|
json={},
|
||||||
|
headers=self.get_headers(webhook),
|
||||||
|
verify=False,
|
||||||
|
timeout=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_user_to_group(self, webhook, user):
|
||||||
|
"""Send request to invite an user to a room or space upon adding them to group.."""
|
||||||
|
join_response = self.join_room(webhook)
|
||||||
|
if join_response.status_code != HTTP_200_OK:
|
||||||
|
logger.error(
|
||||||
|
"Synchronization failed (cannot join room) %s",
|
||||||
|
webhook.url,
|
||||||
|
)
|
||||||
|
return join_response, False
|
||||||
|
|
||||||
|
user_id = self.get_user_id(user)
|
||||||
|
response = session.post(
|
||||||
|
f"{self._get_room_url(webhook.url)}/invite",
|
||||||
|
json={
|
||||||
|
"user_id": user_id,
|
||||||
|
"reason": f"User added to team {webhook.team} on People",
|
||||||
|
},
|
||||||
|
headers=self.get_headers(webhook),
|
||||||
|
verify=False,
|
||||||
|
timeout=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Checks for false negative
|
||||||
|
# (i.e. trying to invite user already in room)
|
||||||
|
webhook_succeeded = False
|
||||||
|
if (
|
||||||
|
response.status_code == HTTP_200_OK
|
||||||
|
or b"is already in the room." in response.content
|
||||||
|
):
|
||||||
|
webhook_succeeded = True
|
||||||
|
|
||||||
|
return response, webhook_succeeded
|
||||||
|
|
||||||
|
def remove_user_from_group(self, webhook, user):
|
||||||
|
"""Send request to kick an user from a room or space upon removing them from group."""
|
||||||
|
join_response = self.join_room(webhook)
|
||||||
|
if join_response.status_code != HTTP_200_OK:
|
||||||
|
logger.error(
|
||||||
|
"Synchronization failed (cannot join room) %s",
|
||||||
|
webhook.url,
|
||||||
|
)
|
||||||
|
return join_response, False
|
||||||
|
|
||||||
|
user_id = self.get_user_id(user)
|
||||||
|
response = session.post(
|
||||||
|
f"{self._get_room_url(webhook.url)}/kick",
|
||||||
|
json={
|
||||||
|
"user_id": user_id,
|
||||||
|
"reason": f"User removed from team {webhook.team} on People",
|
||||||
|
},
|
||||||
|
headers=self.get_headers(webhook),
|
||||||
|
verify=False,
|
||||||
|
timeout=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Checks for false negative
|
||||||
|
# (i.e. trying to remove user who already left the room)
|
||||||
|
webhook_succeeded = False
|
||||||
|
if (
|
||||||
|
response.status_code == HTTP_200_OK
|
||||||
|
or b"The target user is not in the room" in response.content
|
||||||
|
):
|
||||||
|
webhook_succeeded = True
|
||||||
|
|
||||||
|
return response, webhook_succeeded
|
||||||
@@ -39,7 +39,7 @@ class SCIMClient:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
return session.patch(
|
response = session.patch(
|
||||||
webhook.url,
|
webhook.url,
|
||||||
json=payload,
|
json=payload,
|
||||||
headers=webhook.get_headers(),
|
headers=webhook.get_headers(),
|
||||||
@@ -47,6 +47,8 @@ class SCIMClient:
|
|||||||
timeout=3,
|
timeout=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return response, response.ok
|
||||||
|
|
||||||
def remove_user_from_group(self, webhook, user):
|
def remove_user_from_group(self, webhook, user):
|
||||||
"""Remove a user from a group by its ID or email."""
|
"""Remove a user from a group by its ID or email."""
|
||||||
payload = {
|
payload = {
|
||||||
@@ -61,10 +63,12 @@ class SCIMClient:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return session.patch(
|
response = session.patch(
|
||||||
webhook.url,
|
webhook.url,
|
||||||
json=payload,
|
json=payload,
|
||||||
headers=webhook.get_headers(),
|
headers=webhook.get_headers(),
|
||||||
verify=False,
|
verify=False,
|
||||||
timeout=3,
|
timeout=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return response, response.ok
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ import logging
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from core import enums
|
||||||
from core.enums import WebhookStatusChoices
|
from core.enums import WebhookStatusChoices
|
||||||
|
|
||||||
|
from .matrix import MatrixAPIClient
|
||||||
from .scim import SCIMClient
|
from .scim import SCIMClient
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class WebhookSCIMClient:
|
class WebhookClient:
|
||||||
"""Wraps the SCIM client to record call results on webhooks."""
|
"""Wraps the SCIM client to record call results on webhooks."""
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
@@ -26,31 +28,15 @@ class WebhookSCIMClient:
|
|||||||
if not webhook.url:
|
if not webhook.url:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
client = SCIMClient()
|
|
||||||
status = WebhookStatusChoices.FAILURE
|
status = WebhookStatusChoices.FAILURE
|
||||||
try:
|
response, webhook_succeeded = self._get_response_and_status(
|
||||||
response = getattr(client, name)(webhook, user)
|
name, webhook, user
|
||||||
|
)
|
||||||
|
|
||||||
except requests.exceptions.RetryError as exc:
|
if response is not None:
|
||||||
logger.error(
|
extra = {"response": response.content}
|
||||||
"%s synchronization failed due to max retries exceeded with url %s",
|
|
||||||
name,
|
|
||||||
webhook.url,
|
|
||||||
exc_info=exc,
|
|
||||||
)
|
|
||||||
except requests.exceptions.RequestException as exc:
|
|
||||||
logger.error(
|
|
||||||
"%s synchronization failed with %s.",
|
|
||||||
name,
|
|
||||||
webhook.url,
|
|
||||||
exc_info=exc,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
extra = {
|
|
||||||
"response": response.content,
|
|
||||||
}
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
if response.status_code == requests.codes.ok:
|
if webhook_succeeded:
|
||||||
logger.info(
|
logger.info(
|
||||||
"%s synchronization succeeded with %s",
|
"%s synchronization succeeded with %s",
|
||||||
name,
|
name,
|
||||||
@@ -71,5 +57,37 @@ class WebhookSCIMClient:
|
|||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
def _get_client(self, webhook):
|
||||||
|
"""Get client depending on the protocol."""
|
||||||
|
if webhook.protocol == enums.WebhookProtocolChoices.MATRIX:
|
||||||
|
return MatrixAPIClient()
|
||||||
|
|
||||||
scim_synchronizer = WebhookSCIMClient()
|
return SCIMClient()
|
||||||
|
|
||||||
|
def _get_response_and_status(self, name, webhook, user):
|
||||||
|
"""Get response from webhook outside party."""
|
||||||
|
client = self._get_client(webhook)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response, webhook_succeeded = getattr(client, name)(webhook, user)
|
||||||
|
except requests.exceptions.RetryError as exc:
|
||||||
|
logger.error(
|
||||||
|
"%s synchronization failed due to max retries exceeded with url %s",
|
||||||
|
name,
|
||||||
|
webhook.url,
|
||||||
|
exc_info=exc,
|
||||||
|
)
|
||||||
|
except requests.exceptions.RequestException as exc:
|
||||||
|
logger.error(
|
||||||
|
"%s synchronization failed with %s.",
|
||||||
|
name,
|
||||||
|
webhook.url,
|
||||||
|
exc_info=exc,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return response, webhook_succeeded
|
||||||
|
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
|
webhooks_synchronizer = WebhookClient()
|
||||||
|
|||||||
@@ -601,6 +601,11 @@ class Base(Configuration):
|
|||||||
environ_name="DNS_PROVISIONING_API_CREDENTIALS",
|
environ_name="DNS_PROVISIONING_API_CREDENTIALS",
|
||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
)
|
)
|
||||||
|
TCHAP_ACCESS_TOKEN = values.Value(
|
||||||
|
default=None,
|
||||||
|
environ_name="TCHAP_ACCESS_TOKEN",
|
||||||
|
environ_prefix=None,
|
||||||
|
)
|
||||||
|
|
||||||
# Organizations
|
# Organizations
|
||||||
ORGANIZATION_REGISTRATION_ID_VALIDATORS = json.loads(
|
ORGANIZATION_REGISTRATION_ID_VALIDATORS = json.loads(
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ backend:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: backend
|
name: backend
|
||||||
key: MAIL_PROVISIONING_API_CREDENTIALS
|
key: MAIL_PROVISIONING_API_CREDENTIALS
|
||||||
|
TCHAP_ACCESS_TOKEN: service_account_key
|
||||||
command:
|
command:
|
||||||
- "gunicorn"
|
- "gunicorn"
|
||||||
- "-c"
|
- "-c"
|
||||||
|
|||||||
Reference in New Issue
Block a user