Skip to content

Commit c9968b4

Browse files
davinotdavidmarkstos
authored andcommitted
Expose waffle flags through an API route (#1072)
* Add new GET auth/waffle-flags endpoint * Add tests for auth/waffle-flags endpoint * Just forward waffle_json instead of surfacing flags only * Update tests to explicitly pass bearer token headers * Add extra test for fake bearer token
1 parent 33c7637 commit c9968b4

3 files changed

Lines changed: 101 additions & 1 deletion

File tree

src/thunderbird_accounts/authentication/api.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
import logging
99

1010
from django.conf import settings
11+
from mozilla_django_oidc.contrib.drf import OIDCAuthentication
1112
from rest_framework.authentication import SessionAuthentication
1213
from rest_framework.throttling import UserRateThrottle
13-
from rest_framework.permissions import AllowAny
14+
from rest_framework.permissions import AllowAny, IsAuthenticated
1415
import sentry_sdk
16+
from waffle.views import waffle_json
1517

1618
from thunderbird_accounts.authentication.exceptions import (
1719
InvalidDomainError,
@@ -67,6 +69,19 @@ def get_user_profile(request: Request):
6769
return Response(UserProfileSerializer(request.user).data)
6870

6971

72+
@api_view(['GET'])
73+
@authentication_classes([OIDCAuthentication])
74+
@permission_classes([IsAuthenticated])
75+
def get_waffle_flags(request: Request):
76+
"""Return the caller's active waffle flags, switches, and samples.
77+
78+
Callers authenticate with `Authorization: Bearer <keycloak-access-token>`
79+
and we resolve that to the matching local user via OIDCAuthentication.
80+
This is just waffle's own `waffle_json` view wrapped with our token auth.
81+
"""
82+
return waffle_json(request)
83+
84+
7085
@api_view(['GET'])
7186
@authentication_classes([SessionAuthentication])
7287
def get_mfa_methods(request: Request):

src/thunderbird_accounts/authentication/tests/test_api.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from django.urls import reverse
55
from json import JSONDecodeError
66
from unittest.mock import MagicMock, patch
7+
from waffle.models import Flag
78
import uuid
89

910
from django.conf import settings
11+
from django.core.exceptions import SuspiciousOperation
1012
from urllib.parse import quote
1113
from django.test import Client as RequestClient, override_settings
1214
from rest_framework.test import APITestCase, APIClient
@@ -399,3 +401,85 @@ def test_missing_emails_just_error_out(self):
399401
self.assertEqual('no-email', resp_data.get('type'))
400402
test_entry = AllowListEntry.objects.filter(email=email).first()
401403
self.assertIsNone(test_entry)
404+
405+
406+
class WaffleFlagsTestcase(APITestCase):
407+
def setUp(self):
408+
self.client = APIClient()
409+
self.url = reverse('api_waffle_flags')
410+
self.user = User.objects.create(
411+
oidc_id=str(uuid.uuid4()),
412+
recovery_email=f'{uuid.uuid4()}@example.com',
413+
username=f'{uuid.uuid4()}@example.org',
414+
)
415+
416+
Flag.objects.create(name='flag-on-for-everyone', everyone=True)
417+
Flag.objects.create(name='flag-off-for-everyone', everyone=False)
418+
Flag.objects.create(name='flag-on-for-authenticated', authenticated=True)
419+
420+
# Due to the endpoint being gated by OIDCAuthentication
421+
patcher = patch(
422+
'thunderbird_accounts.authentication.middleware.AccountsOIDCBackend.get_userinfo',
423+
side_effect=self._fake_userinfo,
424+
)
425+
patcher.start()
426+
self.addCleanup(patcher.stop)
427+
428+
@staticmethod
429+
def _fake_userinfo(access_token, id_token, payload):
430+
# Mimic a real OIDC provider, which would reject unrecognized/invalid
431+
# access tokens rather than happily returning userinfo for anything.
432+
if not User.objects.filter(oidc_id=access_token).exists():
433+
raise SuspiciousOperation('invalid access token')
434+
435+
return {
436+
'sub': access_token,
437+
'email': f'{access_token}@example.org',
438+
'email_verified': True,
439+
'preferred_username': f'{access_token}@example.org',
440+
}
441+
442+
def test_returns_active_flags_for_authenticated_user(self):
443+
response = self.client.get(self.url, headers={'authorization': f'Bearer {self.user.oidc_id}'})
444+
self.assertEqual(200, response.status_code, response.content)
445+
446+
flags = response.json().get('flags')
447+
self.assertEqual(
448+
{
449+
'flag-on-for-everyone',
450+
'flag-off-for-everyone',
451+
'flag-on-for-authenticated',
452+
},
453+
flags.keys(),
454+
)
455+
self.assertTrue(flags['flag-on-for-everyone']['is_active'])
456+
self.assertFalse(flags['flag-off-for-everyone']['is_active'])
457+
self.assertTrue(flags['flag-on-for-authenticated']['is_active'])
458+
459+
def test_returns_active_flag_for_specific_user_only(self):
460+
other_user = User.objects.create(
461+
oidc_id=str(uuid.uuid4()),
462+
recovery_email=f'{uuid.uuid4()}@example.com',
463+
username=f'{uuid.uuid4()}@example.org',
464+
)
465+
466+
flag = Flag.objects.create(name='flag-on-for-specific-user')
467+
flag.users.add(self.user)
468+
469+
# Authenticate as the user created in the setup step and check that the flag is active
470+
response = self.client.get(self.url, headers={'authorization': f'Bearer {self.user.oidc_id}'})
471+
self.assertEqual(200, response.status_code, response.content)
472+
self.assertTrue(response.json()['flags']['flag-on-for-specific-user']['is_active'])
473+
474+
# Authenticate as the other user and check that the flag is not active
475+
response = self.client.get(self.url, headers={'authorization': f'Bearer {other_user.oidc_id}'})
476+
self.assertEqual(200, response.status_code, response.content)
477+
self.assertFalse(response.json()['flags']['flag-on-for-specific-user']['is_active'])
478+
479+
def test_requires_authentication(self):
480+
response = self.client.get(self.url)
481+
self.assertEqual(401, response.status_code)
482+
483+
def test_returns_401_for_invalid_token(self):
484+
response = self.client.get(self.url, headers={'authorization': 'Bearer invalid-token'})
485+
self.assertEqual(401, response.status_code)

src/thunderbird_accounts/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
path('api/v1/auth/get-profile/', get_user_profile, name='api_get_profile'),
6868
path('api/v1/auth/sign-up/', sign_up, name='api_sign_up'),
6969
path('api/v1/auth/can-i-sign-up/', can_i_sign_up, name='api_can_i_sign_up'),
70+
path('api/v1/auth/waffle-flags/', auth_api.get_waffle_flags, name='api_waffle_flags'),
7071
path('api/v1/auth/mfa/methods/', auth_api.get_mfa_methods, name='api_get_mfa_methods'),
7172
path('api/v1/auth/mfa/totp/setup/start/', auth_api.start_totp_setup, name='api_start_totp_setup'),
7273
path('api/v1/auth/mfa/totp/setup/confirm/', auth_api.confirm_totp_setup, name='api_confirm_totp_setup'),

0 commit comments

Comments
 (0)