Skip to content

Commit 346df99

Browse files
committed
feat: add oidc auth
1 parent ab508a5 commit 346df99

File tree

13 files changed

+459
-13
lines changed

13 files changed

+459
-13
lines changed

commitlint.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const scope = [
1111
module.exports = {
1212
extends: ["@commitlint/config-angular"],
1313
rules: {
14-
"body-empty": [2, "never", false],
1514
"scope-enum": [2, "always", scope],
1615
},
1716
};

controller/sentry/admin.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
confirm_action,
66
)
77
from django.contrib import admin
8+
from django.contrib.auth import get_permission_codename
89
from django.db import models
910
from django.utils import timezone
1011
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
@@ -73,8 +74,8 @@ class AppAdmin(
7374
},
7475
],
7576
]
76-
77-
changelist_actions = ["bump"]
77+
actions = ["bump"]
78+
changelist_actions = []
7879
change_actions = ["bump"]
7980

8081
@takes_instance_or_queryset
@@ -88,6 +89,14 @@ def bump(self, request, queryset, form: BumpForm = None):
8889
active_window_end=new_date,
8990
)
9091

92+
bump.allowed_permissions = ("bump_sample_rate",)
93+
94+
def has_bump_sample_rate_permission(self, request):
95+
"""Does the user have the bump permission?"""
96+
opts = self.opts
97+
codename = get_permission_codename("bump_sample_rate", opts)
98+
return request.user.has_perm("%s.%s" % (opts.app_label, codename))
99+
91100
def save_model(self, request, obj, form, change) -> None:
92101
invalidate_cache(f"/sentry/apps/{obj.reference}/")
93102
return super().save_model(request, obj, form, change)

controller/sentry/auth.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from django.conf import settings
2+
from django.contrib.auth.models import Group
3+
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
4+
5+
DEVELOPER_GROUP = Group.objects.get(name=settings.DEVELOPER_GROUP)
6+
7+
8+
class ControllerOIDCAuthenticationBackend(OIDCAuthenticationBackend):
9+
def create_user(self, claims):
10+
user = super().create_user(claims)
11+
self._set_username(user, claims)
12+
self._set_perms(user)
13+
user.save()
14+
15+
return user
16+
17+
def update_user(self, user, claims):
18+
self._set_username(user, claims)
19+
self._set_perms(user)
20+
user.save()
21+
22+
return user
23+
24+
def _set_username(self, user, claims):
25+
email = claims.get("email")
26+
username = email.split("@")[0]
27+
user.username = claims.get("preferred_username") or username
28+
user.first_name = claims.get("given_name", "")
29+
user.last_name = claims.get("family_name", "")
30+
31+
def _set_perms(self, user):
32+
user.is_staff = True
33+
user.groups.add(DEVELOPER_GROUP)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
[
2+
{
3+
"model": "auth.group",
4+
"pk": 1,
5+
"fields": {
6+
"name": "Developer",
7+
"permissions": [
8+
29,
9+
28
10+
]
11+
}
12+
},
13+
{
14+
"model": "auth.group",
15+
"pk": 2,
16+
"fields": {
17+
"name": "Admin",
18+
"permissions": [
19+
25,
20+
29,
21+
26,
22+
27,
23+
28
24+
]
25+
}
26+
},
27+
{
28+
"model": "auth.group",
29+
"pk": 3,
30+
"fields": {
31+
"name": "Owner",
32+
"permissions": [
33+
1,
34+
2,
35+
3,
36+
4,
37+
9,
38+
10,
39+
11,
40+
12,
41+
5,
42+
6,
43+
7,
44+
8,
45+
13,
46+
14,
47+
15,
48+
16,
49+
17,
50+
18,
51+
19,
52+
20,
53+
25,
54+
29,
55+
26,
56+
27,
57+
28,
58+
21,
59+
22,
60+
23,
61+
24
62+
]
63+
}
64+
},
65+
{
66+
"model": "auth.group",
67+
"pk": 4,
68+
"fields": {
69+
"name": "Viewer",
70+
"permissions": [
71+
28
72+
]
73+
}
74+
}
75+
]

controller/sentry/forms.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
1+
from django.conf import settings
2+
from django.core.exceptions import ValidationError
13
from django.forms import DurationField, FloatField, Form
2-
3-
# WIDGET
4+
from durationwidget.widgets import TimeDurationWidget
45

56

67
class BumpForm(Form):
78
new_sample_rate = FloatField(help_text="Sample rate between 0 and 1")
8-
duration = DurationField(help_text="Duration in second")
9+
duration = DurationField(
10+
widget=TimeDurationWidget(show_days=False, show_seconds=False),
11+
help_text="Duration",
12+
)
13+
14+
def clean_new_sample_rate(self):
15+
data = self.cleaned_data["new_sample_rate"]
16+
if data < 0 or data > 1:
17+
raise ValidationError("new_sample_rate must be between 0 and 1")
18+
return data
19+
20+
def clean_duration(self):
21+
data = self.cleaned_data["duration"]
22+
if (
23+
data.total_seconds() < 0
24+
or data.total_seconds() > settings.MAX_BUMP_TIME_SEC
25+
):
26+
raise ValidationError("duration must be between 0 and 600")
27+
return data
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 4.1.5 on 2023-01-12 12:39
2+
3+
from django.db import migrations
4+
5+
from controller.sentry.migrations import ImportFixture
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("sentry", "0005_alter_app_celery_metrics_alter_app_wsgi_metrics"),
12+
]
13+
14+
operations = [
15+
migrations.AlterModelOptions(
16+
name="app",
17+
options={"permissions": [("bump_sample_rate_app", "Can bump sample rate")]},
18+
),
19+
migrations.RunPython(
20+
*ImportFixture("controller/sentry/fixtures/groups.json")()
21+
),
22+
]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from django.core import serializers
2+
3+
4+
class ImportFixture:
5+
def __init__(self, *files) -> None:
6+
self.files = files
7+
self.format = format
8+
9+
def __call__(self):
10+
return self.load_fixture, self.unload_fixture
11+
12+
def get_objects(self):
13+
for file in self.files:
14+
with open(file) as fixture:
15+
objects = serializers.deserialize(
16+
"json", fixture, ignorenonexistent=True
17+
)
18+
for obj in objects:
19+
yield obj
20+
21+
def load_fixture(self, apps, schema_editor):
22+
for obj in self.get_objects():
23+
obj.save()
24+
25+
def unload_fixture(self, apps, schema_editor):
26+
for obj in self.get_objects():
27+
model = apps.get_model(obj.object._meta.label)
28+
kwargs = dict()
29+
if "id" in obj.object.__dict__:
30+
kwargs.update(id=obj.object.__dict__.get("id"))
31+
elif "slug" in obj.object.__dict__:
32+
kwargs.update(slug=obj.object.__dict__.get("slug"))
33+
else:
34+
kwargs.update(**obj.object.__dict__)
35+
try:
36+
model.objects.get(**kwargs).delete()
37+
except model.DoesNotExist:
38+
pass

controller/sentry/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,6 @@ def merge(self, validated_data):
5353
merger = MERGER[validated_data["type"]]
5454
merger(self, validated_data["data"])
5555
self.last_seen = timezone.now()
56+
57+
class Meta:
58+
permissions = [("bump_sample_rate_app", "Can bump sample rate")]

controller/sentry/utils.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ def invalidate_cache(path=""):
1515
"""
1616

1717
# Bootstrap request:
18-
# request.path should point to the view endpoint you want to invalidate
19-
# request.META must include the correct SERVER_NAME and SERVER_PORT as django uses these in order
20-
# to build a MD5 hashed value for the cache_key. Similarly, we need to artificially set the
21-
# language code on the request to 'en-us' to match the initial creation of the cache_key.
22-
# YMMV regarding the language code.
18+
# request.path should point to the view endpoint you want to invalidate
19+
# request.META must include the correct
20+
# SERVER_NAME and SERVER_PORT as django uses these in order
21+
# to build a MD5 hashed value for the cache_key. Similarly, we need to artificially set the
22+
# language code on the request to 'en-us' to match the initial creation of the cache_key.
23+
# YMMV regarding the language code.
2324
request = HttpRequest()
2425
request.META = settings.CACHE_META_INVALIDATION
2526
request.LANGUAGE_CODE = "en-us"

controller/settings.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"admin_action_tools",
3535
"django.contrib.admin",
3636
"django.contrib.auth",
37+
"mozilla_django_oidc",
3738
"django.contrib.contenttypes",
3839
"django.contrib.sessions",
3940
"django.contrib.messages",
@@ -43,9 +44,35 @@
4344
"widget_tweaks",
4445
"django_better_admin_arrayfield",
4546
"django_json_widget",
47+
"durationwidget",
4648
"controller.sentry",
4749
]
4850

51+
AUTHENTICATION_BACKENDS = (
52+
# "django.contrib.auth.backends.ModelBackend",
53+
"controller.sentry.auth.ControllerOIDCAuthenticationBackend",
54+
)
55+
56+
OIDC_RP_CLIENT_ID = os.getenv("OIDC_RP_CLIENT_ID")
57+
OIDC_RP_CLIENT_SECRET = os.getenv("OIDC_RP_CLIENT_SECRET")
58+
# "<URL of the OIDC OP authorization endpoint>"
59+
OIDC_OP_AUTHORIZATION_ENDPOINT = os.getenv("OIDC_OP_AUTHORIZATION_ENDPOINT")
60+
# "<URL of the OIDC OP token endpoint>"
61+
OIDC_OP_TOKEN_ENDPOINT = os.getenv("OIDC_OP_TOKEN_ENDPOINT")
62+
# "<URL of the OIDC OP userinfo endpoint>"
63+
OIDC_OP_USER_ENDPOINT = os.getenv("OIDC_OP_USER_ENDPOINT")
64+
# "<URL path to redirect to after login>"
65+
LOGIN_REDIRECT_URL = os.getenv("LOGIN_REDIRECT_URL")
66+
# "<URL path to redirect to after logout>"
67+
LOGOUT_REDIRECT_URL = os.getenv("LOGOUT_REDIRECT_URL")
68+
69+
OIDC_RP_SIGN_ALGO = os.getenv("OIDC_RP_SIGN_ALGO", "RS256")
70+
71+
OIDC_OP_JWKS_ENDPOINT = os.getenv("OIDC_OP_JWKS_ENDPOINT")
72+
73+
74+
DEVELOPER_GROUP = os.getenv("DEVELOPER_GROUP", "Developer")
75+
4976

5077
MIDDLEWARE = [
5178
"django.middleware.security.SecurityMiddleware",
@@ -165,3 +192,8 @@
165192
"SERVER_PORT": int(os.getenv("CACHE_META_SERVER_PORT", "8000")),
166193
"HTTP_ACCEPT": os.getenv("CACHE_META_HTTP_ACCEPT", "*/*"),
167194
}
195+
196+
197+
MAX_BUMP_TIME_SEC = int(os.getenv("MAX_BUMP_TIME_SEC", "0"))
198+
if MAX_BUMP_TIME_SEC == 0:
199+
MAX_BUMP_TIME_SEC = 30 * 60 # 30 minutes

0 commit comments

Comments
 (0)