Skip to content

Commit

Permalink
feat(auth): Support migration of auth identity id value
Browse files Browse the repository at this point in the history
Useful when an auth provider needs to migrate a id from a legacy
provider identifying id key to a new key.
  • Loading branch information
evanpurkhiser authored Nov 21, 2017
1 parent d1ecf90 commit 2ee53ad
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 5 deletions.
21 changes: 19 additions & 2 deletions src/sentry/auth/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.utils.translation import ugettext_lazy as _

from sentry.app import locks
from sentry.auth.provider import MigratingIdentityId
from sentry.auth.exceptions import IdentityNotValid
from sentry.models import (
AuditLogEntry, AuditLogEntryEvent, AuthIdentity, AuthProvider, Organization, OrganizationMember,
Expand Down Expand Up @@ -646,20 +647,36 @@ def _finish_login_pipeline(self, identity):
their account.
"""
auth_provider = self.auth_provider
user_id = identity['id']

lock = locks.get(
'sso:auth:{}:{}'.format(
auth_provider.id,
md5_text(identity['id']).hexdigest(),
md5_text(user_id).hexdigest(),
),
duration=5,
)
with TimedRetryPolicy(5)(lock.acquire):
try:
auth_identity = AuthIdentity.objects.select_related('user').get(
auth_provider=auth_provider,
ident=identity['id'],
ident=user_id,
)
except AuthIdentity.DoesNotExist:
auth_identity = None

# Handle migration of identity keys
if not auth_identity and isinstance(user_id, MigratingIdentityId):
try:
auth_identity = AuthIdentity.objects.select_related('user').get(
auth_provider=auth_provider,
ident=user_id.legacy_id,
)
auth_identity.update(ident=user_id.id)
except AuthIdentity.DoesNotExist:
auth_identity = None

if not auth_identity:
return self._handle_unknown_identity(identity)

# If the User attached to this AuthIdentity is not active,
Expand Down
18 changes: 18 additions & 0 deletions src/sentry/auth/provider.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
from __future__ import absolute_import, print_function

import logging
from collections import namedtuple

from .view import ConfigureView


class MigratingIdentityId(namedtuple('MigratingIdentityId', ['id', 'legacy_id'])):
"""
MigratingIdentityId may be used in the ``id`` field of an identity
dictionary to facilitate migrating user identites from one identifying id
to another.
"""
__slots__ = ()

def __unicode__(self):
# Default to id when coercing for query lookup
return self.id


class Provider(object):
"""
A provider indicates how authenticate should happen for a given service,
Expand Down Expand Up @@ -62,6 +76,10 @@ def build_identity(self, state):
The ``email`` and ``id`` keys are required, ``name`` is optional.
The ``id`` may be passed in as a ``MigratingIdentityId`` should the
the id key be migrating from one value to another and have multiple
lookup values.
If the identity can not be constructed an ``IdentityNotValid`` error
should be raised.
"""
Expand Down
8 changes: 5 additions & 3 deletions src/sentry/auth/providers/dummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from django.http import HttpResponse

from sentry.auth import Provider, AuthView
from sentry.auth.provider import MigratingIdentityId


class AskEmail(AuthView):
def dispatch(self, request, helper):
if 'email' in request.POST:
helper.bind_state('email', request.POST['email'])
helper.bind_state('email', request.POST.get('email'))
helper.bind_state('legacy_email', request.POST.get('legacy_email'))
return helper.next_step()

return HttpResponse(DummyProvider.TEMPLATE)
Expand All @@ -23,9 +25,9 @@ def get_auth_pipeline(self):

def build_identity(self, state):
return {
'name': 'Dummy',
'id': state['email'],
'id': MigratingIdentityId(id=state['email'], legacy_id=state.get('legacy_email')),
'email': state['email'],
'name': 'Dummy',
}

def refresh_identity(self, auth_identity):
Expand Down
34 changes: 34 additions & 0 deletions tests/sentry/web/frontend/test_auth_organization_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,3 +652,37 @@ def test_swapped_identities(self):
member2 = OrganizationMember.objects.get(id=member2.id)
assert not getattr(member2.flags, 'sso:linked')
assert getattr(member2.flags, 'sso:invalid')

def test_flow_as_unauthenticated_existing_user_legacy_identity_migration(self):
organization = self.create_organization(name='foo', owner=self.user)
user = self.create_user('bar@example.com')
auth_provider = AuthProvider.objects.create(
organization=organization,
provider='dummy',
)
user_ident = AuthIdentity.objects.create(
auth_provider=auth_provider,
user=user,
ident='foo@example.com',
)

path = reverse('sentry-auth-organization', args=[organization.slug])

resp = self.client.post(path, {'init': True})

assert resp.status_code == 200
assert self.provider.TEMPLATE in resp.content.decode('utf-8')

path = reverse('sentry-auth-sso')

resp = self.client.post(path, {
'email': 'foo@new-domain.com',
'legacy_email': 'foo@example.com'
})

# Ensure the ident was migrated from the legacy identity
updated_ident = AuthIdentity.objects.get(id=user_ident.id)
assert updated_ident.ident == 'foo@new-domain.com'

assert resp.status_code == 302
assert resp['Location'] == 'http://testserver' + reverse('sentry-login')

0 comments on commit 2ee53ad

Please sign in to comment.