<commit_before>from __future__ import absolute_import, print_function

import logging

from uuid import uuid4

from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db import IntegrityError, transaction
from django.http import HttpResponseRedirect
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

from sentry.app import locks
from sentry.auth.exceptions import IdentityNotValid
from sentry.models import (
    AuditLogEntry, AuditLogEntryEvent, AuthIdentity, AuthProvider, Organization, OrganizationMember,
    OrganizationMemberTeam, User, UserEmail
)
from sentry.tasks.auth import email_missing_links
from sentry.utils import auth, metrics
from sentry.utils.redis import clusters
from sentry.utils.hashlib import md5_text
from sentry.utils.http import absolute_uri
from sentry.utils.retries import TimedRetryPolicy
from sentry.web.forms.accounts import AuthenticationForm
from sentry.web.helpers import render_to_response
import sentry.utils.json as json

from . import manager

OK_LINK_IDENTITY = _('You have successfully linked your account to your SSO provider.')

OK_SETUP_SSO = _(
    'SSO has been configured for your organization and any existing members have been sent an email to link their accounts.'
)

ERR_UID_MISMATCH = _('There was an error encountered during authentication.')

ERR_NOT_AUTHED = _('You must be authenticated to link accounts.')

ERR_INVALID_IDENTITY = _('The provider did not return a valid user identity.')


class RedisBackedState(object):
    # Expire the pipeline after 10 minutes of inactivity.
    EXPIRATION_TTL = 10 * 60

    def __init__(self, request):
        self.__dict__['request'] = request

    @property
    def _client(self):
        return clusters.get('default').get_local_client_for_key(self.auth_key)

    @property
    def auth_key(self):
        return self.request.session.get('auth_key')

    def regenerate(self, initial_state):
        auth_key = 'auth:pipeline:{}'.format(uuid4().hex)

        self.request.session['auth_key'] = auth_key
        self.request.session.modified = True

        value = json.dumps(initial_state)
        self._client.setex(auth_key, self.EXPIRATION_TTL, value)

    def clear(self):
        if not self.auth_key:
            return

        self._client.delete(self.auth_key)
        del self.request.session['auth_key']
        self.request.session.modified = True

    def is_valid(self):
        return self.auth_key and self._client.get(self.auth_key)

    def get_state(self):
        if not self.auth_key:
            return None

        state_json = self._client.get(self.auth_key)
        if not state_json:
            return None

        return json.loads(state_json)

    def __getattr__(self, key):
        state = self.get_state()
        return state[key] if state else None

    def __setattr__(self, key, value):
        state = self.get_state()
        if not state:
            return

        state[key] = value
        self._client.setex(self.auth_key, self.EXPIRATION_TTL, json.dumps(state))


class AuthHelper(object):
    """
    Helper class which is passed into AuthView's.

    Designed to link provider and views as well as manage the state and
    pipeline.

    Auth has several flows:

    1. The user is going through provider setup, thus enforcing that they link
       their current account to the new auth identity.
    2. The user is anonymous and creating a brand new account.
    3. The user is anonymous and logging into an existing account.
    4. The user is anonymous and creating a brand new account, but may have an
       existing account that should/could be merged.
    5. The user is authenticated and creating a new identity, thus associating
       it with their current account.
    6. The user is authenticated and creating a new identity, but not linking
       it with their account (thus creating a new account).
    """
    # logging in or registering
    FLOW_LOGIN = 1
    # configuring the provider
    FLOW_SETUP_PROVIDER = 2

    @classmethod
    def get_for_request(cls, request):
        state = RedisBackedState(request)
        if not state.is_valid():
            return None

        organization_id = state.org_id
        if not organization_id:
            logging.info('Invalid SSO data found')
            return None

        flow = state.flow
        auth_provider_id = state.auth_provider
        provider_key = state.provider_key
        if auth_provider_id:
            auth_provider = AuthProvider.objects.get(id=auth_provider_id)
        elif provider_key:
            auth_provider = None

        organization = Organization.objects.get(id=state.org_id)

        return cls(
            request, organization, flow, auth_provider=auth_provider, provider_key=provider_key
        )

    def __init__(self, request, organization, flow, auth_provider=None, provider_key=None):
        assert provider_key or auth_provider

        self.request = request
        self.auth_provider = auth_provider
        self.organization = organization
        self.flow = flow
        self.state = RedisBackedState(request)

        if auth_provider:
            provider = auth_provider.get_provider()
        elif provider_key:
            provider = manager.get(provider_key)
        else:
            raise NotImplementedError

        self.provider = provider
        if flow == self.FLOW_LOGIN:
            self.pipeline = provider.get_auth_pipeline()
        elif flow == self.FLOW_SETUP_PROVIDER:
            self.pipeline = provider.get_setup_pipeline()
        else:
            raise NotImplementedError

        # we serialize the pipeline to be [AuthView().get_ident(), ...] which
        # allows us to determine if the pipeline has changed during the auth
        # flow or if the user is somehow circumventing a chunk of it
        self.signature = md5_text(' '.join(av.get_ident() for av in self.pipeline)).hexdigest()

    def pipeline_is_valid(self):
        if not self.state.is_valid():
            return False
        if self.state.flow not in (self.FLOW_LOGIN, self.FLOW_SETUP_PROVIDER):
            return False
        return self.state.signature == self.signature

    def init_pipeline(self):
        self.state.regenerate({
            'uid': self.request.user.id if self.request.user.is_authenticated() else None,
            'auth_provider': self.auth_provider.id if self.auth_provider else None,
            'provider_key': self.provider.key,
            'org_id': self.organization.id,
            'step_index': 0,
            'signature': self.signature,
            'flow': self.flow,
            'data': {},
        })

    def get_redirect_url(self):
        return absolute_uri(reverse('sentry-auth-sso'))

    def clear_session(self):
        self.state.clear()

    def current_step(self):
        """
        Render the current step.
        """
        step_index = self.state.step_index

        if step_index == len(self.pipeline):
            return self.finish_pipeline()

        return self.pipeline[step_index].dispatch(
            request=self.request,
            helper=self,
        )

    def next_step(self):
        """
        Render the next step.
        """
        self.state.step_index += 1
        return self.current_step()

    def finish_pipeline(self):
        data = self.fetch_state()

        try:
            identity = self.provider.build_identity(data)
        except IdentityNotValid:
            return self.error(ERR_INVALID_IDENTITY)

        if self.state.flow == self.FLOW_LOGIN:
            # create identity and authenticate the user
            response = self._finish_login_pipeline(identity)
        elif self.state.flow == self.FLOW_SETUP_PROVIDER:
            response = self._finish_setup_pipeline(identity)

        return response

    @transaction.atomic
    def _handle_attach_identity(self, identity, member=None):
        """
        Given an already authenticated user, attach or re-attach an identity.
        """
        auth_provider = self.auth_provider
        request = self.request
        user = request.user
        organization = self.organization

        # On SAML we don't have an id when doing the setup so we
        # don't gonna attach the identity if that was not provided
        if not identity:
            return

        try:
            try:
                # prioritize identifying by the SSO provider's user ID
                auth_identity = AuthIdentity.objects.get(
                    auth_provider=auth_provider,
                    ident=identity['id'],
                )
            except AuthIdentity.DoesNotExist:
                # otherwise look for an already attached identity
                # this can happen if the SSO provider's internal ID changes
                auth_identity = AuthIdentity.objects.get(
                    auth_provider=auth_provider,
                    user=user,
                )
        except AuthIdentity.DoesNotExist:
            auth_identity = AuthIdentity.objects.create(
                auth_provider=auth_provider,
                user=user,
                ident=identity['id'],
                data=identity.get('data', {}),
            )
            auth_is_new = True
        else:
            now = timezone.now()

            # TODO(dcramer): this might leave the user with duplicate accounts,<commit_msg>fix(auth): Remove unneeded auth helper identity link case<commit_after>from __future__ import absolute_import, print_function

import logging

from uuid import uuid4

from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db import IntegrityError, transaction
from django.http import HttpResponseRedirect
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

from sentry.app import locks
from sentry.auth.exceptions import IdentityNotValid
from sentry.models import (
    AuditLogEntry, AuditLogEntryEvent, AuthIdentity, AuthProvider, Organization, OrganizationMember,
    OrganizationMemberTeam, User, UserEmail
)
from sentry.tasks.auth import email_missing_links
from sentry.utils import auth, metrics
from sentry.utils.redis import clusters
from sentry.utils.hashlib import md5_text
from sentry.utils.http import absolute_uri
from sentry.utils.retries import TimedRetryPolicy
from sentry.web.forms.accounts import AuthenticationForm
from sentry.web.helpers import render_to_response
import sentry.utils.json as json

from . import manager

OK_LINK_IDENTITY = _('You have successfully linked your account to your SSO provider.')

OK_SETUP_SSO = _(
    'SSO has been configured for your organization and any existing members have been sent an email to link their accounts.'
)

ERR_UID_MISMATCH = _('There was an error encountered during authentication.')

ERR_NOT_AUTHED = _('You must be authenticated to link accounts.')

ERR_INVALID_IDENTITY = _('The provider did not return a valid user identity.')


class RedisBackedState(object):
    # Expire the pipeline after 10 minutes of inactivity.
    EXPIRATION_TTL = 10 * 60

    def __init__(self, request):
        self.__dict__['request'] = request

    @property
    def _client(self):
        return clusters.get('default').get_local_client_for_key(self.auth_key)

    @property
    def auth_key(self):
        return self.request.session.get('auth_key')

    def regenerate(self, initial_state):
        auth_key = 'auth:pipeline:{}'.format(uuid4().hex)

        self.request.session['auth_key'] = auth_key
        self.request.session.modified = True

        value = json.dumps(initial_state)
        self._client.setex(auth_key, self.EXPIRATION_TTL, value)

    def clear(self):
        if not self.auth_key:
            return

        self._client.delete(self.auth_key)
        del self.request.session['auth_key']
        self.request.session.modified = True

    def is_valid(self):
        return self.auth_key and self._client.get(self.auth_key)

    def get_state(self):
        if not self.auth_key:
            return None

        state_json = self._client.get(self.auth_key)
        if not state_json:
            return None

        return json.loads(state_json)

    def __getattr__(self, key):
        state = self.get_state()
        return state[key] if state else None

    def __setattr__(self, key, value):
        state = self.get_state()
        if not state:
            return

        state[key] = value
        self._client.setex(self.auth_key, self.EXPIRATION_TTL, json.dumps(state))


class AuthHelper(object):
    """
    Helper class which is passed into AuthView's.

    Designed to link provider and views as well as manage the state and
    pipeline.

    Auth has several flows:

    1. The user is going through provider setup, thus enforcing that they link
       their current account to the new auth identity.
    2. The user is anonymous and creating a brand new account.
    3. The user is anonymous and logging into an existing account.
    4. The user is anonymous and creating a brand new account, but may have an
       existing account that should/could be merged.
    5. The user is authenticated and creating a new identity, thus associating
       it with their current account.
    6. The user is authenticated and creating a new identity, but not linking
       it with their account (thus creating a new account).
    """
    # logging in or registering
    FLOW_LOGIN = 1
    # configuring the provider
    FLOW_SETUP_PROVIDER = 2

    @classmethod
    def get_for_request(cls, request):
        state = RedisBackedState(request)
        if not state.is_valid():
            return None

        organization_id = state.org_id
        if not organization_id:
            logging.info('Invalid SSO data found')
            return None

        flow = state.flow
        auth_provider_id = state.auth_provider
        provider_key = state.provider_key
        if auth_provider_id:
            auth_provider = AuthProvider.objects.get(id=auth_provider_id)
        elif provider_key:
            auth_provider = None

        organization = Organization.objects.get(id=state.org_id)

        return cls(
            request, organization, flow, auth_provider=auth_provider, provider_key=provider_key
        )

    def __init__(self, request, organization, flow, auth_provider=None, provider_key=None):
        assert provider_key or auth_provider

        self.request = request
        self.auth_provider = auth_provider
        self.organization = organization
        self.flow = flow
        self.state = RedisBackedState(request)

        if auth_provider:
            provider = auth_provider.get_provider()
        elif provider_key:
            provider = manager.get(provider_key)
        else:
            raise NotImplementedError

        self.provider = provider
        if flow == self.FLOW_LOGIN:
            self.pipeline = provider.get_auth_pipeline()
        elif flow == self.FLOW_SETUP_PROVIDER:
            self.pipeline = provider.get_setup_pipeline()
        else:
            raise NotImplementedError

        # we serialize the pipeline to be [AuthView().get_ident(), ...] which
        # allows us to determine if the pipeline has changed during the auth
        # flow or if the user is somehow circumventing a chunk of it
        self.signature = md5_text(' '.join(av.get_ident() for av in self.pipeline)).hexdigest()

    def pipeline_is_valid(self):
        if not self.state.is_valid():
            return False
        if self.state.flow not in (self.FLOW_LOGIN, self.FLOW_SETUP_PROVIDER):
            return False
        return self.state.signature == self.signature

    def init_pipeline(self):
        self.state.regenerate({
            'uid': self.request.user.id if self.request.user.is_authenticated() else None,
            'auth_provider': self.auth_provider.id if self.auth_provider else None,
            'provider_key': self.provider.key,
            'org_id': self.organization.id,
            'step_index': 0,
            'signature': self.signature,
            'flow': self.flow,
            'data': {},
        })

    def get_redirect_url(self):
        return absolute_uri(reverse('sentry-auth-sso'))

    def clear_session(self):
        self.state.clear()

    def current_step(self):
        """
        Render the current step.
        """
        step_index = self.state.step_index

        if step_index == len(self.pipeline):
            return self.finish_pipeline()

        return self.pipeline[step_index].dispatch(
            request=self.request,
            helper=self,
        )

    def next_step(self):
        """
        Render the next step.
        """
        self.state.step_index += 1
        return self.current_step()

    def finish_pipeline(self):
        data = self.fetch_state()

        try:
            identity = self.provider.build_identity(data)
        except IdentityNotValid:
            return self.error(ERR_INVALID_IDENTITY)

        if self.state.flow == self.FLOW_LOGIN:
            # create identity and authenticate the user
            response = self._finish_login_pipeline(identity)
        elif self.state.flow == self.FLOW_SETUP_PROVIDER:
            response = self._finish_setup_pipeline(identity)

        return response

    @transaction.atomic
    def _handle_attach_identity(self, identity, member=None):
        """
        Given an already authenticated user, attach or re-attach an identity.
        """
        auth_provider = self.auth_provider
        request = self.request
        user = request.user
        organization = self.organization

        try:
            try:
                # prioritize identifying by the SSO provider's user ID
                auth_identity = AuthIdentity.objects.get(
                    auth_provider=auth_provider,
                    ident=identity['id'],
                )
            except AuthIdentity.DoesNotExist:
                # otherwise look for an already attached identity
                # this can happen if the SSO provider's internal ID changes
                auth_identity = AuthIdentity.objects.get(
                    auth_provider=auth_provider,
                    user=user,
                )
        except AuthIdentity.DoesNotExist:
            auth_identity = AuthIdentity.objects.create(
                auth_provider=auth_provider,
                user=user,
                ident=identity['id'],
                data=identity.get('data', {}),
            )
            auth_is_new = True
        else:
            now = timezone.now()

            # TODO(dcramer): this might leave the user with duplicate accounts,
{'fraction_non_alphanumeric': 0.07361636392691631, 'fraction_numerical': 0.0021307196505619772, 'mean_word_length': 2.406023222060958, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1277379', 'n_tokens_mistral': 5120, 'n_tokens_neox': 4968, 'n_words': 1476}