Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/thunderbird_accounts/authentication/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
from urllib.parse import quote

from django.conf import settings
from mozilla_django_oidc.contrib.drf import OIDCAuthentication
from rest_framework.authentication import SessionAuthentication
from rest_framework.throttling import UserRateThrottle
from rest_framework.permissions import AllowAny
from rest_framework.permissions import AllowAny, IsAuthenticated
import sentry_sdk
from waffle.views import waffle_json

from thunderbird_accounts.authentication.exceptions import (
InvalidDomainError,
Expand Down Expand Up @@ -66,6 +68,19 @@ def get_user_profile(request: Request):
return Response(UserProfileSerializer(request.user).data)


@api_view(['GET'])
@authentication_classes([OIDCAuthentication])
@permission_classes([IsAuthenticated])
def get_waffle_flags(request: Request):
"""Return the caller's active waffle flags, switches, and samples.

Callers authenticate with `Authorization: Bearer <keycloak-access-token>`
and we resolve that to the matching local user via OIDCAuthentication.
This is just waffle's own `waffle_json` view wrapped with our token auth.
"""
return waffle_json(request)


@api_view(['GET'])
@authentication_classes([SessionAuthentication])
def get_mfa_methods(request: Request):
Expand Down
74 changes: 74 additions & 0 deletions src/thunderbird_accounts/authentication/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.urls import reverse
from json import JSONDecodeError
from unittest.mock import MagicMock, patch
from waffle.models import Flag
import uuid

from django.conf import settings
Expand Down Expand Up @@ -353,3 +354,76 @@ def test_missing_emails_just_error_out(self):
self.assertEqual('no-email', resp_data.get('type'))
test_entry = AllowListEntry.objects.filter(email=email).first()
self.assertIsNone(test_entry)


class WaffleFlagsTestcase(APITestCase):
def setUp(self):
self.client = APIClient()
self.url = reverse('api_waffle_flags')
self.user = User.objects.create(
oidc_id=str(uuid.uuid4()),
recovery_email=f'{uuid.uuid4()}@example.com',
username=f'{uuid.uuid4()}@example.org',
)

Flag.objects.create(name='flag-on-for-everyone', everyone=True)
Flag.objects.create(name='flag-off-for-everyone', everyone=False)
Flag.objects.create(name='flag-on-for-authenticated', authenticated=True)

# Due to the endpoint being gated by OIDCAuthentication
patcher = patch(
'thunderbird_accounts.authentication.middleware.AccountsOIDCBackend.get_userinfo',
side_effect=self._fake_userinfo,
)
patcher.start()
self.addCleanup(patcher.stop)

@staticmethod
def _fake_userinfo(access_token, id_token, payload):
return {
'sub': access_token,
'email': f'{access_token}@example.org',
'email_verified': True,
'preferred_username': f'{access_token}@example.org',
}

def test_returns_active_flags_for_authenticated_user(self):
response = self.client.get(self.url, headers={'authorization': f'Bearer {self.user.oidc_id}'})
self.assertEqual(200, response.status_code, response.content)

flags = response.json().get('flags')
self.assertEqual(
{
'flag-on-for-everyone',
'flag-off-for-everyone',
'flag-on-for-authenticated',
},
flags.keys(),
)
self.assertTrue(flags['flag-on-for-everyone']['is_active'])
self.assertFalse(flags['flag-off-for-everyone']['is_active'])
self.assertTrue(flags['flag-on-for-authenticated']['is_active'])

def test_returns_active_flag_for_specific_user_only(self):
other_user = User.objects.create(
oidc_id=str(uuid.uuid4()),
recovery_email=f'{uuid.uuid4()}@example.com',
username=f'{uuid.uuid4()}@example.org',
)

flag = Flag.objects.create(name='flag-on-for-specific-user')
flag.users.add(self.user)

# Authenticate as the user created in the setup step and check that the flag is active
response = self.client.get(self.url, headers={'authorization': f'Bearer {self.user.oidc_id}'})
self.assertEqual(200, response.status_code, response.content)
self.assertTrue(response.json()['flags']['flag-on-for-specific-user']['is_active'])

# Authenticate as the other user and check that the flag is not active
response = self.client.get(self.url, headers={'authorization': f'Bearer {other_user.oidc_id}'})
self.assertEqual(200, response.status_code, response.content)
self.assertFalse(response.json()['flags']['flag-on-for-specific-user']['is_active'])

def test_requires_authentication(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last suggestion, a test for a bogus bearer token / bogus auth value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, thanks!

response = self.client.get(self.url)
self.assertEqual(401, response.status_code)
1 change: 1 addition & 0 deletions src/thunderbird_accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
path('api/v1/auth/get-profile/', get_user_profile, name='api_get_profile'),
path('api/v1/auth/sign-up/', sign_up, name='api_sign_up'),
path('api/v1/auth/can-i-sign-up/', can_i_sign_up, name='api_can_i_sign_up'),
path('api/v1/auth/waffle-flags/', auth_api.get_waffle_flags, name='api_waffle_flags'),
path('api/v1/auth/mfa/methods/', auth_api.get_mfa_methods, name='api_get_mfa_methods'),
path('api/v1/auth/mfa/totp/setup/start/', auth_api.start_totp_setup, name='api_start_totp_setup'),
path('api/v1/auth/mfa/totp/setup/confirm/', auth_api.confirm_totp_setup, name='api_confirm_totp_setup'),
Expand Down
Loading