Add basic organization moved exception handling and middleware

This commit is contained in:
Michael Derynck 2022-10-20 15:04:58 -06:00
parent 0a1a9ab4d8
commit febe1b2185
9 changed files with 129 additions and 1 deletions

View file

@ -18,6 +18,7 @@ from .exceptions import InvalidToken
from .models import ApiAuthToken, PluginAuthToken, ScheduleExportAuthToken, SlackAuthToken, UserScheduleExportAuthToken
from .models.mobile_app_auth_token import MobileAppAuthToken
from .models.mobile_app_verification_token import MobileAppVerificationToken
from ..user_management.models.region import OrganizationMovedException
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
@ -46,6 +47,10 @@ class ApiTokenAuthentication(BaseAuthentication):
auth_token = self.model.validate_token_string(token)
except InvalidToken:
raise exceptions.AuthenticationFailed("Invalid token.")
if auth_token.organization.migration_destination is not None:
raise OrganizationMovedException(auth_token.organization)
return auth_token.user, auth_token
@ -167,6 +172,9 @@ class ScheduleExportAuthentication(BaseAuthentication):
except InvalidToken:
raise exceptions.AuthenticationFailed("Invalid token.")
if auth_token.organization.migration_destination is not None:
raise OrganizationMovedException(auth_token.organization)
if auth_token.schedule.public_primary_key != public_primary_key:
raise exceptions.AuthenticationFailed("Invalid schedule export token for schedule")
@ -197,6 +205,9 @@ class UserScheduleExportAuthentication(BaseAuthentication):
except InvalidToken:
raise exceptions.AuthenticationFailed("Invalid token")
if auth_token.organization.migration_destination is not None:
raise OrganizationMovedException(auth_token.organization)
if auth_token.user.public_primary_key != public_primary_key:
raise exceptions.AuthenticationFailed("Invalid schedule export token for user")

View file

@ -7,6 +7,8 @@ from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db import OperationalError
from apps.user_management.models.region import OrganizationMovedException
logger = logging.getLogger(__name__)
@ -64,6 +66,9 @@ class AlertChannelDefiningMixin(object):
logger.info("Cache is empty!")
raise
if alert_receive_channel.organization.migration_destination is not None:
raise OrganizationMovedException(alert_receive_channel.organization)
del kwargs["alert_channel_key"]
kwargs["alert_receive_channel"] = alert_receive_channel

View file

@ -0,0 +1,41 @@
import logging
import re
import requests
from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin
from apps.user_management.models.region import OrganizationMovedException
from common.api_helpers.utils import create_engine_url
logger = logging.getLogger(__name__)
class OrganizationMovedMiddleware(MiddlewareMixin):
def process_exception(self, request, exception):
if isinstance(exception, OrganizationMovedException):
region = exception.organization.migration_destination
url = create_engine_url(request.path, override_base=region.oncall_backend_url)
if request.META['QUERY_STRING']:
url = f"{url}?{request.META['QUERY_STRING']}"
regex = re.compile('^HTTP_')
headers = dict(
(regex.sub('', header), value) for (header, value) in request.META.items() if header.startswith('HTTP_')
)
if request.method == "GET":
response = requests.get(url, headers=headers)
elif request.method == "POST":
response = requests.post(url, data=request.body, headers=headers)
elif request.method == "PUT":
response = requests.put(url, data=request.body, headers=headers)
elif request.method == "DELETE":
response = requests.delete(url, headers=headers)
elif request.method == "OPTIONS":
response = requests.options(url, headers=headers)
response.raise_for_status()
return HttpResponse(response.content, status=response.status_code)

View file

@ -0,0 +1,32 @@
# Generated by Django 3.2.15 on 2022-10-20 18:45
import apps.user_management.models.region
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('user_management', '0004_organization_region_slug'),
]
operations = [
migrations.CreateModel(
name='Region',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('public_primary_key', models.CharField(default=apps.user_management.models.region.generate_public_primary_key_for_region, max_length=20, unique=True, validators=[django.core.validators.MinLengthValidator(13)])),
('name', models.CharField(max_length=300)),
('slug', models.CharField(max_length=300, unique=True)),
('oncall_backend_url', models.URLField()),
('is_default', models.BooleanField(default=False)),
],
),
migrations.AddField(
model_name='organization',
name='migration_destination',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='regions', to='user_management.region'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.15 on 2022-10-20 18:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user_management', '0005_auto_20221020_1845'),
]
operations = [
migrations.AlterField(
model_name='region',
name='slug',
field=models.CharField(max_length=50, unique=True),
),
]

View file

@ -1,3 +1,4 @@
from .user import User # noqa: F401, isort: skip
from .organization import Organization # noqa: F401
from .region import Region # noqa: F401
from .team import Team # noqa: F401

View file

@ -59,6 +59,7 @@ class Organization(MaintainableObject):
on_delete=models.SET_NULL,
related_name="regions",
default=None,
null=True,
)
grafana_url = models.URLField()

View file

@ -1,10 +1,24 @@
import logging
from django.conf import settings
from django.core.validators import MinLengthValidator
from django.db import models
from rest_framework.request import Request
from rest_framework.response import Response
from apps.user_management.models import Organization
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
logger = logging.getLogger(__name__)
class OrganizationMovedException(Exception):
def __init__(self, organization: Organization):
self.organization = organization
def generate_public_primary_key_for_region():
prefix = "R"
new_public_primary_key = generate_public_primary_key(prefix)
@ -19,6 +33,10 @@ def generate_public_primary_key_for_region():
return new_public_primary_key
def redirect_organization_request(organization: Organization, request: Request):
logger.info("**** Redirect! ****")
class Region(models.Model):
public_primary_key = models.CharField(
max_length=20,
@ -28,6 +46,6 @@ class Region(models.Model):
)
name = models.CharField(max_length=300)
slug = models.CharField(max_length=300, unique=True)
slug = models.CharField(max_length=50, unique=True)
oncall_backend_url = models.URLField()
is_default = models.BooleanField(default=False)

View file

@ -237,6 +237,7 @@ MIDDLEWARE = [
"social_django.middleware.SocialAuthExceptionMiddleware",
"apps.social_auth.middlewares.SocialAuthAuthCanceledExceptionMiddleware",
"apps.integrations.middlewares.IntegrationExceptionMiddleware",
"apps.user_management.middlewares.OrganizationMovedMiddleware",
]
LOG_REQUEST_ID_HEADER = "HTTP_X_CLOUD_TRACE_CONTEXT"