def authenticate(self, request):
        set_custom_metric("BearerAuthentication", "Failed")  # default value
        if not self.get_user_info_url():
            logger.warning('The setting OAUTH2_USER_INFO_URL is invalid!')
            set_custom_metric("BearerAuthentication", "NoURL")
            return None
        set_custom_metric("BearerAuthentication_user_info_url",
                          self.get_user_info_url())
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() != b'bearer':
            set_custom_metric("BearerAuthentication", "None")
            return None

        if len(auth) == 1:
            raise exceptions.AuthenticationFailed(
                'Invalid token header. No credentials provided.')
        if len(auth) > 2:
            raise exceptions.AuthenticationFailed(
                'Invalid token header. Token string should not contain spaces.'
            )

        output = self.authenticate_credentials(auth[1].decode('utf8'))
        set_custom_metric("BearerAuthentication", "Success")
        return output
Exemple #2
0
    def get_schedules_with_target_date_by_bin_and_orgs(
        self, order_by='enrollment__user__id'
    ):
        """
        Returns Schedules with the target_date, related to Users whose id matches the bin_num, and filtered by org_list.

        Arguments:
        order_by -- string for field to sort the resulting Schedules by
        """
        target_day = _get_datetime_beginning_of_day(self.target_datetime)
        schedule_day_equals_target_day_filter = {
            'courseenrollment__schedule__{}__gte'.format(self.schedule_date_field): target_day,
            'courseenrollment__schedule__{}__lt'.format(self.schedule_date_field): target_day + datetime.timedelta(days=1),
        }
        users = User.objects.filter(
            courseenrollment__is_active=True,
            is_active=True,
            **schedule_day_equals_target_day_filter
        ).annotate(
            id_mod=self.bin_num_for_user_id(F('id'))
        ).filter(
            id_mod=self.bin_num
        )

        schedule_day_equals_target_day_filter = {
            '{}__gte'.format(self.schedule_date_field): target_day,
            '{}__lt'.format(self.schedule_date_field): target_day + datetime.timedelta(days=1),
        }
        schedules = Schedule.objects.select_related(
            'enrollment__user__profile',
            'enrollment__course',
            'enrollment__fbeenrollmentexclusion',
        ).filter(
            Q(enrollment__course__end__isnull=True) | Q(
                enrollment__course__end__gte=self.current_datetime
            ),
            self.experience_filter,
            enrollment__user__in=users,
            enrollment__is_active=True,
            active=True,
            **schedule_day_equals_target_day_filter
        ).order_by(order_by)

        schedules = self.filter_by_org(schedules)

        if "read_replica" in settings.DATABASES:
            schedules = schedules.using("read_replica")

        LOG.info(u'Query = %r', schedules.query.sql_with_params())

        with function_trace('schedule_query_set_evaluation'):
            # This will run the query and cache all of the results in memory.
            num_schedules = len(schedules)

        LOG.info(u'Number of schedules = %d', num_schedules)

        # This should give us a sense of the volume of data being processed by each task.
        set_custom_metric('num_schedules', num_schedules)

        return schedules
Exemple #3
0
    def process_request(self, request):
        """
        Reconstitute the full JWT and add a new cookie on the request object.
        """
        use_jwt_cookie_requested = request.META.get(USE_JWT_COOKIE_HEADER)
        header_payload_cookie = request.COOKIES.get(jwt_cookie_header_payload_name())
        signature_cookie = request.COOKIES.get(jwt_cookie_signature_name())

        if not use_jwt_cookie_requested:
            metric_value = 'not-requested'
        elif header_payload_cookie and signature_cookie:
            # Reconstitute JWT auth cookie if split cookies are available and jwt cookie
            # authentication was requested by the client.
            request.COOKIES[jwt_cookie_name()] = '{}{}{}'.format(
                header_payload_cookie,
                JWT_DELIMITER,
                signature_cookie,
            )
            metric_value = 'success'
        elif header_payload_cookie or signature_cookie:
            # Log unexpected case of only finding one cookie.
            if not header_payload_cookie:
                log_message, metric_value = self._get_missing_cookie_message_and_metric(
                    jwt_cookie_header_payload_name()
                )
            if not signature_cookie:
                log_message, metric_value = self._get_missing_cookie_message_and_metric(
                    jwt_cookie_signature_name()
                )
            log.warning(log_message)
        else:
            metric_value = 'missing-both'

        monitoring.set_custom_metric('request_jwt_cookie', metric_value)
    def _set_request_auth_type_metric(self, request):
        """
        Add metric 'request_auth_type' for the authentication type used.

        NOTE: This is a best guess at this point.  Possible values include:
            no-user
            unauthenticated
            jwt/bearer/other-token-type
            session-or-unknown (catch all)

        """
        if 'HTTP_AUTHORIZATION' in request.META and request.META[
                'HTTP_AUTHORIZATION']:
            token_parts = request.META['HTTP_AUTHORIZATION'].split()
            # Example: "JWT eyJhbGciO..."
            if len(token_parts) == 2:
                auth_type = token_parts[0].lower(
                )  # 'jwt' or 'bearer' (for example)
            else:
                auth_type = 'other-token-type'
        elif not hasattr(request, 'user') or not request.user:
            auth_type = 'no-user'
        elif not request.user.is_authenticated:
            auth_type = 'unauthenticated'
        else:
            auth_type = 'session-or-unknown'
        monitoring.set_custom_metric('request_auth_type', auth_type)
 def _set_request_referer_metric(self, request):
     """
     Add metric 'request_referer' for http referer.
     """
     if 'HTTP_REFERER' in request.META and request.META['HTTP_REFERER']:
         monitoring.set_custom_metric('request_referer',
                                      request.META['HTTP_REFERER'])
Exemple #6
0
    def lms_user_id_with_metric(self, usage=None):
        """
        Returns the LMS user_id, or None if not found. Also sets a metric with the result.

        Arguments:
            usage (string): Optional. A description of how the returned id will be used. This will be included in log
                messages if the LMS user id cannot be found.

        Side effect:
            If found, writes custom metric: 'ecommerce_found_lms_user_id'
            If not found, writes custom metric: 'ecommerce_missing_lms_user_id'
        """
        # Read the lms_user_id from the ecommerce_user.
        lms_user_id = self.lms_user_id
        if lms_user_id:
            monitoring_utils.set_custom_metric('ecommerce_found_lms_user_id',
                                               lms_user_id)
            return lms_user_id

        # Could not find the lms_user_id
        monitoring_utils.set_custom_metric('ecommerce_missing_lms_user_id',
                                           self.id)
        log.warn(u'Could not find lms_user_id for user %s for %s', self.id,
                 usage)
        return None
Exemple #7
0
    def lms_user_id(self):
        """
        Returns the LMS user_id, or None if not found.
        """
        # JWT cookie is used with API calls from new microfrontends. This is not persisted.
        lms_user_id = self._get_lms_user_id_from_jwt_cookie()
        if lms_user_id:
            return lms_user_id

        # This is persisted to the database during any new oAuth+SSO flow.
        lms_user_id = self._get_lms_user_id_from_social_auth()
        if lms_user_id:
            return lms_user_id

        # Server-to-server calls from LMS to ecommerce use a specially crafted JWT.
        lms_user_id = self._get_lms_user_id_from_tracking_context()
        if lms_user_id:
            return lms_user_id

        # If we get here, it means either:
        # 1. The user has an old social_auth session created before the LMS user_id was written to the database, or
        # 2. This could be a server-to-server call that isn't properly handled, or
        # 3. Some other unknown flow.
        monitoring_utils.set_custom_metric(
            'ecommerce_user_missing_lms_user_id', self.id)
        return None
Exemple #8
0
    def is_flag_active(self, flag_name, check_before_waffle_callback=None):
        """
        Returns and caches whether the provided flag is active.

        If the flag value is already cached in the request, it is returned.
        If check_before_waffle_callback is supplied, it is called before
            checking waffle.
        If check_before_waffle_callback returns None, or if it is not supplied,
            then waffle is used to check the flag.

        Important: Caching for the check_before_waffle_callback must be handled
            by the callback itself.

        Note: A waffle flag's default is False if not defined. If you think you
            need the default to be True, see the module docstring for
            alternatives.

        Arguments:
            flag_name (String): The name of the flag to check.
            check_before_waffle_callback (function): (Optional) A function that
                will be checked before continuing on to waffle. If
                check_before_waffle_callback(namespaced_flag_name) returns True
                or False, it is returned. If it returns None, then waffle is
                used.

        """
        # validate arguments
        namespaced_flag_name = self._namespaced_name(flag_name)

        if check_before_waffle_callback:
            value = check_before_waffle_callback(namespaced_flag_name)
            if value is not None:
                # Do not cache value for the callback, because the key might be different.
                # The callback needs to handle its own caching if it wants it.
                self._set_waffle_flag_metric(namespaced_flag_name, value)
                return value

        value = self._cached_flags.get(namespaced_flag_name)
        if value is not None:
            self._set_waffle_flag_metric(namespaced_flag_name, value)
            return value

        request = crum.get_current_request()
        if not request:
            log.warning(u"%sFlag '%s' accessed without a request",
                        self.log_prefix, namespaced_flag_name)
            # Return the Flag's Everyone value if not in a request context.
            # Note: this skips the cache as the value might be different
            # in a normal request context. This case seems to occur when
            # a page redirects to a 404, or for celery workers.
            value = self._is_flag_active_for_everyone(namespaced_flag_name)
            self._set_waffle_flag_metric(namespaced_flag_name, value)
            set_custom_metric('warn_flag_no_request_return_value', value)
            return value

        value = flag_is_active(request, namespaced_flag_name)
        self._cached_flags[namespaced_flag_name] = value

        self._set_waffle_flag_metric(namespaced_flag_name, value)
        return value
Exemple #9
0
    def _get_jwt_builder(self, user, is_client_restricted):
        """ Creates and returns a JWTBuilder object for creating JWTs. """

        # If JWT scope enforcement is enabled, we need to sign tokens
        # given to restricted applications with a key that
        # other IDAs do not have access to. This prevents restricted
        # applications from getting access to API endpoints available
        # on other IDAs which have not yet been protected with the
        # scope-related DRF permission classes. Once all endpoints have
        # been protected, we can enable all IDAs to use the same new
        # (asymmetric) key.
        # TODO: ARCH-162
        use_asymmetric_key = ENFORCE_JWT_SCOPES.is_enabled(
        ) and is_client_restricted
        monitoring_utils.set_custom_metric('oauth_asymmetric_jwt',
                                           use_asymmetric_key)

        log.info("Using Asymmetric JWT: %s", use_asymmetric_key)

        return JwtBuilder(
            user,
            asymmetric=use_asymmetric_key,
            secret=settings.JWT_AUTH['JWT_SECRET_KEY'],
            issuer=settings.JWT_AUTH['JWT_ISSUER'],
        )
Exemple #10
0
    def process_request(self, request):
        """
        Reconstitute the full JWT and add a new cookie on the request object.
        """
        use_jwt_cookie_requested = request.META.get(USE_JWT_COOKIE_HEADER)
        header_payload_cookie = request.COOKIES.get(jwt_cookie_header_payload_name())
        signature_cookie = request.COOKIES.get(jwt_cookie_signature_name())

        if not use_jwt_cookie_requested:
            metric_value = 'not-requested'
        elif header_payload_cookie and signature_cookie:
            # Reconstitute JWT auth cookie if split cookies are available and jwt cookie
            # authentication was requested by the client.
            request.COOKIES[jwt_cookie_name()] = '{}{}{}'.format(
                header_payload_cookie,
                JWT_DELIMITER,
                signature_cookie,
            )
            metric_value = 'success'
        elif header_payload_cookie or signature_cookie:
            # Log unexpected case of only finding one cookie.
            if not header_payload_cookie:
                log_message, metric_value = self._get_missing_cookie_message_and_metric(
                    jwt_cookie_header_payload_name()
                )
            if not signature_cookie:
                log_message, metric_value = self._get_missing_cookie_message_and_metric(
                    jwt_cookie_signature_name()
                )
            log.warning(log_message)
        else:
            metric_value = 'missing-both'

        monitoring.set_custom_metric('request_jwt_cookie', metric_value)
Exemple #11
0
    def lms_user_id(self):
        """
        Returns the LMS user_id, or None if not found.
        """
        # JWT cookie is used with API calls from new microfrontends. This is not persisted.
        # TODO: Rename ``_get_lms_user_id_from_jwt_cookie`` to ``_get_lms_user_id_from_jwt``
        #   and update to use new method to be added to JwtAuthentication in edx-drf-extensions
        #   to get the decoded JWT used for authentication, no matter where it came from.
        #   See https://github.com/edx/edx-drf-extensions/pull/69#discussion_r286618922
        lms_user_id = self._get_lms_user_id_from_jwt_cookie()
        if lms_user_id:
            return lms_user_id

        # This is persisted to the database during any new oAuth+SSO flow.
        lms_user_id = self._get_lms_user_id_from_social_auth()
        if lms_user_id:
            return lms_user_id

        # Server-to-server calls from LMS to ecommerce use a specially crafted JWT.
        lms_user_id = self._get_lms_user_id_from_tracking_context()
        if lms_user_id:
            return lms_user_id

        # If we get here, it means either:
        # 1. The user has an old social_auth session created before the LMS user_id was written to the database, or
        # 2. This could be a server-to-server call that isn't properly handled, or
        # 3. Some other unknown flow.
        monitoring_utils.set_custom_metric('ecommerce_user_missing_lms_user_id', self.id)
        return None
Exemple #12
0
def activate_account(activation_key):
    """Activate a user's account.

    Args:
        activation_key (unicode): The activation key the user received via email.

    Returns:
        None

    Raises:
        errors.UserNotAuthorized
        errors.UserAPIInternalError: the operation failed due to an unexpected error.

    """
    # TODO: Confirm this `activate_account` is only used for tests. If so, this should not be used for tests, and we
    # should instead use the `activate_account` used for /activate.
    set_custom_metric('user_api_activate_account', 'True')
    if waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
        raise errors.UserAPIInternalError(SYSTEM_MAINTENANCE_MSG)
    try:
        registration = Registration.objects.get(activation_key=activation_key)
    except Registration.DoesNotExist:
        raise errors.UserNotAuthorized
    else:
        # This implicitly saves the registration
        registration.activate()
Exemple #13
0
    def _set_request_auth_type_metric(self, request):
        """
        Add metric 'request_auth_type' for the authentication type used.

        NOTE: This is a best guess at this point.  Possible values include:
            no-user
            unauthenticated
            jwt/bearer/other-token-type
            session-or-unknown (catch all)

        """
        if 'HTTP_AUTHORIZATION' in request.META and request.META['HTTP_AUTHORIZATION']:
            token_parts = request.META['HTTP_AUTHORIZATION'].split()
            # Example: "JWT eyJhbGciO..."
            if len(token_parts) == 2:
                auth_type = token_parts[0].lower()  # 'jwt' or 'bearer' (for example)
            else:
                auth_type = 'other-token-type'
        elif not hasattr(request, 'user') or not request.user:
            auth_type = 'no-user'
        elif not request.user.is_authenticated:
            auth_type = 'unauthenticated'
        else:
            auth_type = 'session-or-unknown'
        monitoring.set_custom_metric('request_auth_type', auth_type)
Exemple #14
0
def _compute_time_fields(expires_in):
    """
    Returns (iat, exp) tuple to be used as time-related values in a token.
    """
    now = int(time())
    expires_in = expires_in or settings.JWT_AUTH['JWT_EXPIRATION']
    set_custom_metric('jwt_expires_in', expires_in)
    return now, now + expires_in
Exemple #15
0
def _compute_time_fields(expires_in):
    """
    Returns (iat, exp) tuple to be used as time-related values in a token.
    """
    now = int(time())
    expires_in = expires_in or settings.JWT_AUTH['JWT_EXPIRATION']
    set_custom_metric('jwt_expires_in', expires_in)
    return now, now + expires_in
Exemple #16
0
    def get_adapter(self, request):
        """
        Returns the appropriate adapter based on the OAuth client linked to the request.
        """
        client_id = self._get_client_id(request)
        monitoring_utils.set_custom_metric('oauth_client_id', client_id)

        return self.dot_adapter
    def get_schedules_with_target_date_by_bin_and_orgs(
        self, order_by='enrollment__user__id'
    ):
        """
        Returns Schedules with the target_date, related to Users whose id matches the bin_num, and filtered by org_list.

        Arguments:
        order_by -- string for field to sort the resulting Schedules by
        """
        target_day = _get_datetime_beginning_of_day(self.target_datetime)
        schedule_day_equals_target_day_filter = {
            'courseenrollment__schedule__{}__gte'.format(self.schedule_date_field): target_day,
            'courseenrollment__schedule__{}__lt'.format(self.schedule_date_field): target_day + datetime.timedelta(days=1),
        }
        users = User.objects.filter(
            courseenrollment__is_active=True,
            **schedule_day_equals_target_day_filter
        ).annotate(
            id_mod=F('id') % self.num_bins
        ).filter(
            id_mod=self.bin_num
        )

        schedule_day_equals_target_day_filter = {
            '{}__gte'.format(self.schedule_date_field): target_day,
            '{}__lt'.format(self.schedule_date_field): target_day + datetime.timedelta(days=1),
        }
        schedules = Schedule.objects.select_related(
            'enrollment__user__profile',
            'enrollment__course',
        ).filter(
            Q(enrollment__course__end__isnull=True) | Q(
                enrollment__course__end__gte=self.current_datetime
            ),
            self.experience_filter,
            enrollment__user__in=users,
            enrollment__is_active=True,
            active=True,
            **schedule_day_equals_target_day_filter
        ).order_by(order_by)

        schedules = self.filter_by_org(schedules)

        if "read_replica" in settings.DATABASES:
            schedules = schedules.using("read_replica")

        LOG.info(u'Query = %r', schedules.query.sql_with_params())

        with function_trace('schedule_query_set_evaluation'):
            # This will run the query and cache all of the results in memory.
            num_schedules = len(schedules)

        LOG.info(u'Number of schedules = %d', num_schedules)

        # This should give us a sense of the volume of data being processed by each task.
        set_custom_metric('num_schedules', num_schedules)

        return schedules
Exemple #18
0
    def _set_request_user_id_metric(self, request):
        """
        Add request_user_id metric

        Metrics:
             request_user_id
        """
        if hasattr(request, 'user') and hasattr(request.user, 'id') and request.user.id:
            monitoring.set_custom_metric('request_user_id', request.user.id)
    def _set_request_user_id_metric(self, request):
        """
        Add request_user_id metric

        Metrics:
             request_user_id
        """
        if hasattr(request, 'user') and hasattr(request.user,
                                                'id') and request.user.id:
            monitoring.set_custom_metric('request_user_id', request.user.id)
Exemple #20
0
    def dispatch(self, request, *args, **kwargs):
        response = super(AccessTokenView, self).dispatch(request, *args, **kwargs)

        token_type = request.POST.get('token_type', 'no_token_type_supplied').lower()
        monitoring_utils.set_custom_metric('oauth_token_type', token_type)
        monitoring_utils.set_custom_metric('oauth_grant_type', request.POST.get('grant_type', ''))

        if response.status_code == 200 and token_type == 'jwt':
            response.content = self._build_jwt_response_from_access_token_response(request, response)

        return response
Exemple #21
0
    def request(self, method, url, **kwargs):  # pylint: disable=arguments-differ
        """
        Overrides Session.request to ensure that the session is authenticated.

        Note: Typically, users of the client won't call this directly, but will
        instead use Session.get or Session.post.

        """
        set_custom_metric('api_client', 'OAuthAPIClient')
        self._ensure_authentication()
        return super(OAuthAPIClient, self).request(method, url, **kwargs)
Exemple #22
0
    def dispatch(self, request, *args, **kwargs):
        response = super(AccessTokenView, self).dispatch(request, *args, **kwargs)

        token_type = request.POST.get('token_type', 'no_token_type_supplied').lower()
        monitoring_utils.set_custom_metric('oauth_token_type', token_type)
        monitoring_utils.set_custom_metric('oauth_grant_type', request.POST.get('grant_type', ''))

        if response.status_code == 200 and token_type == 'jwt':
            response.content = self._build_jwt_response_from_access_token_response(request, response)

        return response
Exemple #23
0
    def get_basket(self, request):
        """ Return the open basket for this request """
        # pylint: disable=protected-access
        if request._basket_cache is not None:
            monitoring_utils.set_custom_metric('basket_id',
                                               request._basket_cache.id)
            return request._basket_cache

        manager = Basket.open
        cookie_key = self.get_cookie_key(request)
        cookie_basket = self.get_cookie_basket(cookie_key, request, manager)

        if hasattr(request, 'user') and request.user.is_authenticated():
            # Signed-in user: if they have a cookie basket too, it means
            # that they have just signed in and we need to merge their cookie
            # basket into their user basket, then delete the cookie.
            try:
                basket, __ = manager.get_or_create(owner=request.user,
                                                   site=request.site)
            except Basket.MultipleObjectsReturned:
                # Not sure quite how we end up here with multiple baskets.
                # We merge them and create a fresh one
                old_baskets = list(
                    manager.filter(owner=request.user, site=request.site))
                basket = old_baskets[0]
                for other_basket in old_baskets[1:]:
                    self.merge_baskets(basket, other_basket)

            # Assign user onto basket to prevent further SQL queries when
            # basket.owner is accessed.
            basket.owner = request.user

            if cookie_basket:
                self.merge_baskets(basket, cookie_basket)
                request.cookies_to_delete.append(cookie_key)

        elif cookie_basket:
            # Anonymous user with a basket tied to the cookie
            basket = cookie_basket
        else:
            # Anonymous user with no basket - instantiate a new basket instance.  No need to save yet.
            basket = Basket(site=request.site)

        # Cache basket instance for the duration of this request
        request._basket_cache = basket
        if request._basket_cache is not None:
            monitoring_utils.set_custom_metric('basket_id',
                                               request._basket_cache.id)
        else:  # pragma: no cover
            pass

        return basket
Exemple #24
0
 def get_view_for_backend(self, backend):
     """
     Return the appropriate view from the requested backend.
     """
     if backend == self.dot_adapter.backend:
         monitoring_utils.set_custom_metric('oauth_view', 'dot')
         return self.dot_view.as_view()
     elif backend == self.dop_adapter.backend:
         monitoring_utils.set_custom_metric('oauth_view', 'dop')
         return self.dop_view.as_view()
     else:
         raise KeyError(
             'Failed to dispatch view. Invalid backend {}'.format(backend))
Exemple #25
0
    def __init__(self,
                 url,
                 signing_key=None,
                 username=None,
                 full_name=None,
                 email=None,
                 timeout=5,
                 issuer=None,
                 expires_in=30,
                 tracking_context=None,
                 oauth_access_token=None,
                 session=None,
                 jwt=None,
                 **kwargs):
        """
        EdxRestApiClient is deprecated. Use OAuthAPIClient instead.

        Instantiate a new client. You can pass extra kwargs to Slumber like
        'append_slash'.

        Raises:
            ValueError: If a URL is not provided.

        """
        set_custom_metric('api_client', 'EdxRestApiClient')
        if not url:
            raise ValueError('An API url must be supplied!')

        if jwt:
            auth = SuppliedJwtAuth(jwt)
        elif oauth_access_token:
            auth = BearerAuth(oauth_access_token)
        elif signing_key and username:
            auth = JwtAuth(username,
                           full_name,
                           email,
                           signing_key,
                           issuer=issuer,
                           expires_in=expires_in,
                           tracking_context=tracking_context)
        else:
            auth = None

        session = session or requests.Session()
        session.headers['User-Agent'] = self.user_agent()

        session.timeout = timeout
        super(EdxRestApiClient, self).__init__(url,
                                               session=session,
                                               auth=auth,
                                               **kwargs)
Exemple #26
0
    def _get_lms_user_id_from_tracking_context(self):
        """
        Return LMS user_id passed through tracking_context, if found.
        Returns None if not found.

        Side effect:
            If found, writes custom metric: 'lms_user_id_tracking_context'
        """
        # Return lms_user_id passed through tracking_context, if found.
        tracking_context = self.tracking_context or {}
        lms_user_id_tracking_context = tracking_context.get('lms_user_id')
        if lms_user_id_tracking_context:
            monitoring_utils.set_custom_metric('lms_user_id_tracking_context', lms_user_id_tracking_context)
            return lms_user_id_tracking_context
Exemple #27
0
def _annotate_for_monitoring(message_type, course_key, target_day_str, day_offset):
    """
    Set custom metrics in monitoring to make it easier to identify what messages are being sent and why.
    """
    # This identifies the type of message being sent, for example: schedules.recurring_nudge3.
    set_custom_metric('message_name', '{0}.{1}'.format(message_type.app_label, message_type.name))
    # The domain name of the site we are sending the message for.
    set_custom_metric('course_key', course_key)
    # The date we are processing data for.
    set_custom_metric('target_day', target_day_str)
    # The number of days relative to the current date to process data for.
    set_custom_metric('day_offset', day_offset)
    # A unique identifier for this batch of messages being sent.
    set_custom_metric('send_uuid', message_type.uuid)
    def process_view(self, request, view_func, view_args, view_kwargs):  # pylint: disable=unused-argument
        """
        Reconstitute the full JWT and add a new cookie on the request object.
        """
        assert hasattr(
            request, 'session'
        ), "The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."  # noqa E501 line too long

        use_jwt_cookie_requested = request.META.get(USE_JWT_COOKIE_HEADER)
        header_payload_cookie = request.COOKIES.get(
            jwt_cookie_header_payload_name())
        signature_cookie = request.COOKIES.get(jwt_cookie_signature_name())

        is_set_request_user_for_jwt_cookie_enabled = get_setting(
            ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE)
        if use_jwt_cookie_requested and is_set_request_user_for_jwt_cookie_enabled:
            # DRF does not set request.user until process_response. This makes it available in process_view.
            # For more info, see https://github.com/jpadilla/django-rest-framework-jwt/issues/45#issuecomment-74996698
            request.user = SimpleLazyObject(
                lambda: _get_user_from_jwt(request, view_func))

        if not use_jwt_cookie_requested:
            metric_value = 'not-requested'
        elif header_payload_cookie and signature_cookie:
            # Reconstitute JWT auth cookie if split cookies are available and jwt cookie
            # authentication was requested by the client.
            request.COOKIES[jwt_cookie_name()] = '{}{}{}'.format(
                header_payload_cookie,
                JWT_DELIMITER,
                signature_cookie,
            )
            metric_value = 'success'
        elif header_payload_cookie or signature_cookie:
            # Log unexpected case of only finding one cookie.
            if not header_payload_cookie:
                log_message, metric_value = self._get_missing_cookie_message_and_metric(
                    jwt_cookie_header_payload_name())
            if not signature_cookie:
                log_message, metric_value = self._get_missing_cookie_message_and_metric(
                    jwt_cookie_signature_name())
            log.warning(log_message)
        else:
            metric_value = 'missing-both'
            log.warning(
                'Both JWT auth cookies missing. JWT auth cookies will not be reconstituted.'
            )

        monitoring.set_custom_metric('request_jwt_cookie', metric_value)
 def _set_view_func_compare_metric(self, view_func):
     """
     Set temporary metric to ensure that the view_func of `process_view` always matches
     the one from using `resolve` on the request.
     """
     try:
         view_func_module = view_func.__module__
         cached_response = DEFAULT_REQUEST_CACHE.get_cached_response(
             self._VIEW_FUNC_MODULE_METRIC_CACHE_KEY)
         if cached_response.is_found:
             view_func_compare = 'success' if view_func_module == cached_response.value else view_func_module
         else:
             view_func_compare = 'missing'
         set_custom_metric('temp_view_func_compare', view_func_compare)
     except Exception as e:
         set_custom_metric('temp_view_func_compare_error', e)
def get_user_tracking_id(user):
    """
    Returns the tracking ID associated with this user or None. The tracking ID
    is cached.
    """
    cache_key = _get_tracking_cache_key(user)
    tracking_id = cache.get(cache_key)

    # if tracking ID was not found in cache, fetch and cache it
    if tracking_id is None:
        # first, attempt to get the tracking id from an oauth2 social_auth record
        tracking_id = _get_lms_user_id_from_social_auth(user)
        cache.set(cache_key, tracking_id)

    set_custom_metric('tracking_id', tracking_id)
    return tracking_id
Exemple #31
0
    def has_permission(self, request, view):
        """
        Check if the OAuth client associated with auth token in current request has permission to access
        the information for provider
        """
        provider_id = view.kwargs.get('provider_id')
        if not request.auth or not provider_id:
            # doesn't have access token or no provider_id specified
            return False

        try:
            ProviderApiPermissions.objects.get(client__pk=request.auth.client_id, provider_id=provider_id)
        except ProviderApiPermissions.DoesNotExist:
            return False

        set_custom_metric('deprecated_ThirdPartyAuthProviderApiPermission', True)
        return True
Exemple #32
0
def _encode_and_sign(payload, use_asymmetric_key, secret):
    """Encode and sign the provided payload."""
    set_custom_metric('jwt_is_asymmetric', use_asymmetric_key)
    keys = jwk.KEYS()

    if use_asymmetric_key:
        serialized_keypair = json.loads(settings.JWT_AUTH['JWT_PRIVATE_SIGNING_JWK'])
        keys.add(serialized_keypair)
        algorithm = settings.JWT_AUTH['JWT_SIGNING_ALGORITHM']
    else:
        key = secret if secret else settings.JWT_AUTH['JWT_SECRET_KEY']
        keys.add({'key': key, 'kty': 'oct'})
        algorithm = settings.JWT_AUTH['JWT_ALGORITHM']

    data = json.dumps(payload)
    jws = JWS(data, alg=algorithm)
    return jws.sign_compact(keys=keys)
Exemple #33
0
def _encode_and_sign(payload, use_asymmetric_key, secret):
    """Encode and sign the provided payload."""
    set_custom_metric('jwt_is_asymmetric', use_asymmetric_key)
    keys = jwk.KEYS()

    if use_asymmetric_key:
        serialized_keypair = json.loads(settings.JWT_AUTH['JWT_PRIVATE_SIGNING_JWK'])
        keys.add(serialized_keypair)
        algorithm = settings.JWT_AUTH['JWT_SIGNING_ALGORITHM']
    else:
        key = secret if secret else settings.JWT_AUTH['JWT_SECRET_KEY']
        keys.add({'key': key, 'kty': 'oct'})
        algorithm = settings.JWT_AUTH['JWT_ALGORITHM']

    data = json.dumps(payload)
    jws = JWS(data, alg=algorithm)
    return jws.sign_compact(keys=keys)
Exemple #34
0
def jwt_decode_handler(token):
    """
    Attempt to decode the given token with each of the configured JWT issuers.

    Args:
        token (str): The JWT to decode.

    Returns:
        dict: The JWT's payload.

    Raises:
        InvalidTokenError: If the token is invalid, or if none of the
            configured issuer/secret-key combos can properly decode the token.

    """

    # First, try ecommerce decoder that handles multiple issuers.
    # See ARCH-276 for details of removing additional issuers and retiring this
    # custom jwt_decode_handler.
    try:
        jwt_payload = _ecommerce_jwt_decode_handler_multiple_issuers(token)
        monitoring_utils.set_custom_metric(JWT_DECODE_HANDLER_METRIC_KEY,
                                           'ecommerce-multiple-issuers')
        return jwt_payload
    except Exception:  # pylint: disable=broad-except
        if waffle.switch_is_active(
                'jwt_decode_handler.log_exception.ecommerce-multiple-issuers'):
            logger.info(
                'Failed to use ecommerce multiple issuer jwt_decode_handler.',
                exc_info=True)

    # Next, try jwt_decode_handler from edx_drf_extensions
    # Note: this jwt_decode_handler can handle asymmetric keys, but only a
    #   single issuer. Therefore, the LMS must be the first configured issuer.
    try:
        jwt_payload = edx_drf_extensions_jwt_decode_handler(token)
        monitoring_utils.set_custom_metric(JWT_DECODE_HANDLER_METRIC_KEY,
                                           'edx-drf-extensions')
        return jwt_payload
    except Exception:  # pylint: disable=broad-except
        # continue and try again
        if waffle.switch_is_active(
                'jwt_decode_handler.log_exception.edx-drf-extensions'):
            logger.info('Failed to use edx-drf-extensions jwt_decode_handler.',
                        exc_info=True)
        raise
Exemple #35
0
    def add_lms_user_id(self,
                        missing_metric_key,
                        called_from,
                        allow_missing=False):
        """
        If this user does not already have an LMS user id, look for the id in social auth. If the id can be found,
        add it to the user and save the user.

        The LMS user_id may already be present for the user. It may have been added from the jwt (see the
        EDX_DRF_EXTENSIONS.JWT_PAYLOAD_USER_ATTRIBUTE_MAPPING settings) or by a previous call to this method.

        Arguments:
            missing_metric_key (String): Key name for metric that will be created if the LMS user id cannot be found.
            called_from (String): Descriptive string describing the caller. This will be included in log messages.
            allow_missing (boolean): True if the LMS user id is allowed to be missing. This affects the log messages,
            custom metrics, and (in combination with the allow_missing_lms_user_id switch), whether an
            MissingLmsUserIdException is raised. Defaults to False.

        Side effect:
            If the LMS id cannot be found, writes custom metrics.
        """
        if not self.lms_user_id:
            # Check for the LMS user id in social auth
            lms_user_id_social_auth, social_auth_id = self._get_lms_user_id_from_social_auth(
            )
            if lms_user_id_social_auth:
                self.lms_user_id = lms_user_id_social_auth
                self.save()
                log.info(
                    u'Saving lms_user_id from social auth with id %s for user %s. Called from %s',
                    social_auth_id, self.id, called_from)
            else:
                # Could not find the LMS user id
                if allow_missing or waffle.switch_is_active(
                        ALLOW_MISSING_LMS_USER_ID):
                    monitoring_utils.set_custom_metric(
                        'ecommerce_missing_lms_user_id_allowed', self.id)
                    monitoring_utils.set_custom_metric(
                        missing_metric_key + '_allowed', self.id)

                    error_msg = (
                        u'Could not find lms_user_id for user {user_id}. Missing lms_user_id is allowed. '
                        u'Called from {called_from}'.format(
                            user_id=self.id, called_from=called_from))
                    log.info(error_msg, exc_info=True)
                else:
                    monitoring_utils.set_custom_metric(
                        'ecommerce_missing_lms_user_id', self.id)
                    monitoring_utils.set_custom_metric(missing_metric_key,
                                                       self.id)

                    error_msg = u'Could not find lms_user_id for user {user_id}. Called from {called_from}'.format(
                        user_id=self.id, called_from=called_from)
                    log.error(error_msg, exc_info=True)

                    raise MissingLmsUserIdException(error_msg)
Exemple #36
0
    def _get_lms_user_id_from_social_auth(self):
        """
        Return LMS user_id passed through social auth, if found.
        Returns None if not found.

        Side effect:
            If found, writes custom metric: 'lms_user_id_social_auth'
        """
        try:
            lms_user_id_social_auth = self.social_auth.first().extra_data[u'user_id']  # pylint: disable=no-member
            if lms_user_id_social_auth:
                monitoring_utils.set_custom_metric('lms_user_id_social_auth', lms_user_id_social_auth)
                return lms_user_id_social_auth
            else:  # pragma: no cover
                pass  # allows coverage skip for just this case.
        except Exception:  # pylint: disable=broad-except
            pass
Exemple #37
0
    def has_permission(self, request, view):
        """
        Check for permissions by matching the configured API key and header
        Allow the request if and only if settings.EDX_API_KEY is set and
        the X-Edx-Api-Key HTTP header is present in the request and
        matches the setting.
        """
        api_key = getattr(settings, "EDX_API_KEY", None)

        if api_key is not None and request.META.get(
                "HTTP_X_EDX_API_KEY") == api_key:
            audit_log("ApiKeyHeaderPermission used",
                      path=request.path,
                      ip=request.META.get("REMOTE_ADDR"))
            set_custom_metric('deprecated_api_key_header', True)
            return True

        return False
Exemple #38
0
    def _set_request_user_agent_metrics(self, request):
        """
        Add metrics for user agent for python.

        Metrics:
             request_user_agent
             request_client_name: The client name from edx-rest-api-client calls.
        """
        if 'HTTP_USER_AGENT' in request.META and request.META['HTTP_USER_AGENT']:
            user_agent = request.META['HTTP_USER_AGENT']
            monitoring.set_custom_metric('request_user_agent', user_agent)
            if user_agent:
                # Example agent string from edx-rest-api-client:
                #    python-requests/2.9.1 edx-rest-api-client/1.7.2 ecommerce
                #    See https://github.com/edx/edx-rest-api-client/commit/692903c30b157f7a4edabc2f53aae1742db3a019
                user_agent_parts = user_agent.split()
                if len(user_agent_parts) == 3 and user_agent_parts[1].startswith('edx-rest-api-client/'):
                    monitoring.set_custom_metric('request_client_name', user_agent_parts[2])
Exemple #39
0
    def _get_jwt_builder(self, user, is_client_restricted):
        """ Creates and returns a JWTBuilder object for creating JWTs. """

        # If JWT scope enforcement is enabled, we need to sign tokens
        # given to restricted applications with a key that
        # other IDAs do not have access to. This prevents restricted
        # applications from getting access to API endpoints available
        # on other IDAs which have not yet been protected with the
        # scope-related DRF permission classes. Once all endpoints have
        # been protected, we can enable all IDAs to use the same new
        # (asymmetric) key.
        # TODO: ARCH-162
        use_asymmetric_key = ENFORCE_JWT_SCOPES.is_enabled() and is_client_restricted
        monitoring_utils.set_custom_metric('oauth_asymmetric_jwt', use_asymmetric_key)

        log.info("Using Asymmetric JWT: %s", use_asymmetric_key)

        return JwtBuilder(
            user,
            asymmetric=use_asymmetric_key,
            secret=settings.JWT_AUTH['JWT_SECRET_KEY'],
            issuer=settings.JWT_AUTH['JWT_ISSUER'],
        )
Exemple #40
0
    def get_adapter(self, request):
        """
        Returns the appropriate adapter based on the OAuth client linked to the request.
        """
        client_id = self._get_client_id(request)
        monitoring_utils.set_custom_metric('oauth_client_id', client_id)

        if dot_models.Application.objects.filter(client_id=client_id).exists():
            monitoring_utils.set_custom_metric('oauth_adapter', 'dot')
            return self.dot_adapter
        else:
            monitoring_utils.set_custom_metric('oauth_adapter', 'dop')
            return self.dop_adapter
Exemple #41
0
def _recalculate_subsection_grade(self, **kwargs):
    """
    Updates a saved subsection grade.

    Keyword Arguments:
        user_id (int): id of applicable User object
        anonymous_user_id (int, OPTIONAL): Anonymous ID of the User
        course_id (string): identifying the course
        usage_id (string): identifying the course block
        only_if_higher (boolean): indicating whether grades should
            be updated only if the new raw_earned is higher than the
            previous value.
        expected_modified_time (serialized timestamp): indicates when the task
            was queued so that we can verify the underlying data update.
        score_deleted (boolean): indicating whether the grade change is
            a result of the problem's score being deleted.
        event_transaction_id (string): uuid identifying the current
            event transaction.
        event_transaction_type (string): human-readable type of the
            event at the root of the current event transaction.
        score_db_table (ScoreDatabaseTableEnum): database table that houses
            the changed score. Used in conjunction with expected_modified_time.
    """
    try:
        course_key = CourseLocator.from_string(kwargs['course_id'])
        scored_block_usage_key = UsageKey.from_string(kwargs['usage_id']).replace(course_key=course_key)

        set_custom_metrics_for_course_key(course_key)
        set_custom_metric('usage_id', unicode(scored_block_usage_key))

        # The request cache is not maintained on celery workers,
        # where this code runs. So we take the values from the
        # main request cache and store them in the local request
        # cache. This correlates model-level grading events with
        # higher-level ones.
        set_event_transaction_id(kwargs.get('event_transaction_id'))
        set_event_transaction_type(kwargs.get('event_transaction_type'))

        # Verify the database has been updated with the scores when the task was
        # created. This race condition occurs if the transaction in the task
        # creator's process hasn't committed before the task initiates in the worker
        # process.
        has_database_updated = _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs)

        if not has_database_updated:
            raise DatabaseNotReadyError

        _update_subsection_grades(
            course_key,
            scored_block_usage_key,
            kwargs['only_if_higher'],
            kwargs['user_id'],
            kwargs['score_deleted'],
        )
    except Exception as exc:
        if not isinstance(exc, KNOWN_RETRY_ERRORS):
            log.info("tnl-6244 grades unexpected failure: {}. task id: {}. kwargs={}".format(
                repr(exc),
                self.request.id,
                kwargs,
            ))
        raise self.retry(kwargs=kwargs, exc=exc)
Exemple #42
0
def change_enrollment(request, check_access=True):
    """
    Modify the enrollment status for the logged-in user.

    TODO: This is lms specific and does not belong in common code.

    The request parameter must be a POST request (other methods return 405)
    that specifies course_id and enrollment_action parameters. If course_id or
    enrollment_action is not specified, if course_id is not valid, if
    enrollment_action is something other than "enroll" or "unenroll", if
    enrollment_action is "enroll" and enrollment is closed for the course, or
    if enrollment_action is "unenroll" and the user is not enrolled in the
    course, a 400 error will be returned. If the user is not logged in, 403
    will be returned; it is important that only this case return 403 so the
    front end can redirect the user to a registration or login page when this
    happens. This function should only be called from an AJAX request, so
    the error messages in the responses should never actually be user-visible.

    Args:
        request (`Request`): The Django request object

    Keyword Args:
        check_access (boolean): If True, we check that an accessible course actually
            exists for the given course_key before we enroll the student.
            The default is set to False to avoid breaking legacy code or
            code with non-standard flows (ex. beta tester invitations), but
            for any standard enrollment flow you probably want this to be True.

    Returns:
        Response

    """
    # Get the user
    user = request.user

    # Ensure the user is authenticated
    if not user.is_authenticated:
        return HttpResponseForbidden()

    # Ensure we received a course_id
    action = request.POST.get("enrollment_action")
    if 'course_id' not in request.POST:
        return HttpResponseBadRequest(_("Course id not specified"))

    try:
        course_id = CourseKey.from_string(request.POST.get("course_id"))
    except InvalidKeyError:
        log.warning(
            u"User %s tried to %s with invalid course id: %s",
            user.username,
            action,
            request.POST.get("course_id"),
        )
        return HttpResponseBadRequest(_("Invalid course id"))

    # Allow us to monitor performance of this transaction on a per-course basis since we often roll-out features
    # on a per-course basis.
    monitoring_utils.set_custom_metric('course_id', text_type(course_id))

    if action == "enroll":
        # Make sure the course exists
        # We don't do this check on unenroll, or a bad course id can't be unenrolled from
        if not modulestore().has_course(course_id):
            log.warning(
                u"User %s tried to enroll in non-existent course %s",
                user.username,
                course_id
            )
            return HttpResponseBadRequest(_("Course id is invalid"))

        # Record the user's email opt-in preference
        if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'):
            _update_email_opt_in(request, course_id.org)

        available_modes = CourseMode.modes_for_course_dict(course_id)

        # Check whether the user is blocked from enrolling in this course
        # This can occur if the user's IP is on a global blacklist
        # or if the user is enrolling in a country in which the course
        # is not available.
        redirect_url = embargo_api.redirect_if_blocked(
            course_id, user=user, ip_address=get_ip(request),
            url=request.path
        )
        if redirect_url:
            return HttpResponse(redirect_url)

        if CourseEntitlement.check_for_existing_entitlement_and_enroll(user=user, course_run_key=course_id):
            return HttpResponse(reverse('courseware', args=[unicode(course_id)]))

        # Check that auto enrollment is allowed for this course
        # (= the course is NOT behind a paywall)
        if CourseMode.can_auto_enroll(course_id):
            # Enroll the user using the default mode (audit)
            # We're assuming that users of the course enrollment table
            # will NOT try to look up the course enrollment model
            # by its slug.  If they do, it's possible (based on the state of the database)
            # for no such model to exist, even though we've set the enrollment type
            # to "audit".
            try:
                enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes)
                if enroll_mode:
                    CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode)
            except Exception:  # pylint: disable=broad-except
                return HttpResponseBadRequest(_("Could not enroll"))

        # If we have more than one course mode or professional ed is enabled,
        # then send the user to the choose your track page.
        # (In the case of no-id-professional/professional ed, this will redirect to a page that
        # funnels users directly into the verification / payment flow)
        if CourseMode.has_verified_mode(available_modes) or CourseMode.has_professional_mode(available_modes):
            return HttpResponse(
                reverse("course_modes_choose", kwargs={'course_id': text_type(course_id)})
            )

        # Otherwise, there is only one mode available (the default)
        return HttpResponse()
    elif action == "unenroll":
        enrollment = CourseEnrollment.get_enrollment(user, course_id)
        if not enrollment:
            return HttpResponseBadRequest(_("You are not enrolled in this course"))

        certificate_info = cert_info(user, enrollment.course_overview)
        if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES:
            return HttpResponseBadRequest(_("Your certificate prevents you from unenrolling from this course"))

        CourseEnrollment.unenroll(user, course_id)
        REFUND_ORDER.send(sender=None, course_enrollment=enrollment)
        return HttpResponse()
    else:
        return HttpResponseBadRequest(_("Enrollment action is invalid"))
Exemple #43
0
 def _set_request_referer_metric(self, request):
     """
     Add metric 'request_referer' for http referer.
     """
     if 'HTTP_REFERER' in request.META and request.META['HTTP_REFERER']:
         monitoring.set_custom_metric('request_referer', request.META['HTTP_REFERER'])
Exemple #44
0
def _annotate_for_monitoring(message_type, site, bin_num, target_day_str, day_offset):
    # This identifies the type of message being sent, for example: schedules.recurring_nudge3.
    set_custom_metric('message_name', '{0}.{1}'.format(message_type.app_label, message_type.name))
    # The domain name of the site we are sending the message for.
    set_custom_metric('site', site.domain)
    # This is the "bin" of data being processed. We divide up the work into chunks so that we don't tie up celery
    # workers for too long. This could help us identify particular bins that are problematic.
    set_custom_metric('bin', bin_num)
    # The date we are processing data for.
    set_custom_metric('target_day', target_day_str)
    # The number of days relative to the current date to process data for.
    set_custom_metric('day_offset', day_offset)
    # A unique identifier for this batch of messages being sent.
    set_custom_metric('send_uuid', message_type.uuid)
Exemple #45
0
def _annonate_send_task_for_monitoring(msg):
    # A unique identifier for this batch of messages being sent.
    set_custom_metric('send_uuid', msg.send_uuid)
    # A unique identifier for this particular message.
    set_custom_metric('uuid', msg.uuid)