# -*- coding: utf-8 -*-

"""
    eve.auth
    ~~~~~~~~

    Allow API endpoints to be secured via BasicAuth and derivates.

    :copyright: (c) 2016 by Nicola Iarocci.
    :license: BSD, see LICENSE for more details.
"""
from flask import request, Response, current_app as app, g, abort
from functools import wraps


def requires_auth(endpoint_class):
    """ Enables Authorization logic for decorated functions.

    :param endpoint_class: the 'class' to which the decorated endpoint belongs
                           to.  Can be 'resource' (resource endpoint), 'item'
                           (item endpoint) and 'home' for the API entry point.

    .. versionchanged:: 0.0.7
       Passing the 'resource' argument when inoking auth.authenticate()

    .. versionchanged:: 0.0.5
       Support for Cross-Origin Resource Sharing (CORS): 'OPTIONS' request
       method is now public by default. The actual method ('GET', etc.) will
       still be protected if so configured.

    .. versionadded:: 0.0.4
    """
    def fdec(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            if endpoint_class == 'resource' or endpoint_class == 'item':
                # find resource name in f's args
                if args:
                    resource_name = args[0]
                elif kwargs.get('resource'):
                    resource_name = kwargs.get('resource')
                else:
                    raise ValueError("'requires_auth(%s)' decorated functions "
                                     "must include resource in args or kwargs"
                                     % endpoint_class)

                # fetch resource or item auth configuration
                resource = app.config['DOMAIN'].get(resource_name)
                if resource is None:
                    abort(404)
                if endpoint_class == 'resource':
                    public = resource['public_methods']
                    roles = list(resource['allowed_roles'])
                    if request.method in ['GET', 'HEAD', 'OPTIONS']:
                        roles += resource['allowed_read_roles']
                    else:
                        roles += resource['allowed_write_roles']
                elif endpoint_class == 'item':
                    public = resource['public_item_methods']
                    roles = list(resource['allowed_item_roles'])
                    if request.method in ['GET', 'HEAD', 'OPTIONS']:
                        roles += resource['allowed_item_read_roles']
                    else:
                        roles += resource['allowed_item_write_roles']
                auth = resource_auth(resource_name)
            else:
                # home
                resource_name = resource = None
                public = app.config['PUBLIC_METHODS'] + ['OPTIONS']
                roles = list(app.config['ALLOWED_ROLES'])
                if request.method in ['GET', 'OPTIONS']:
                    roles += app.config['ALLOWED_READ_ROLES']
                else:
                    roles += app.config['ALLOWED_WRITE_ROLES']
                auth = app.auth
            if auth and request.method not in public:
                if not auth.authorized(roles, resource_name, request.method):
                    return auth.authenticate()
            return f(*args, **kwargs)
        return decorated
    return fdec


class BasicAuth(object):
    """ Implements Basic AUTH logic. Should be subclassed to implement custom
    authentication checking.

    .. versionchanged:: 0.6
       Add mongo_prefix getter and setter methods.

    .. versionchanged:: 0.4
       ensure all errors returns a parseable body #366.
       auth.request_auth_value replaced with getter and setter methods which
       rely on flask's 'g' object, for enhanced thread-safity.

    .. versionchanged:: 0.1.1
        auth.request_auth_value is now used to store the auth_field value.

    .. versionchanged:: 0.0.9
       Support for user_id property.

    .. versionchanged:: 0.0.7
       Support for 'resource' argument.

    .. versionadded:: 0.0.4
    """
    def set_mongo_prefix(self, value):
        g.mongo_prefix = value

    def get_mongo_prefix(self):
        return g.get('mongo_prefix')

    def set_request_auth_value(self, value):
        g.auth_value = value

    def get_request_auth_value(self):
        return g.get('auth_value')

    def check_auth(self, username, password, allowed_roles, resource, method):
        """ This function is called to check if a username / password
        combination is valid. Must be overridden with custom logic.

        :param username: username provided with current request.
        :param password: password provided with current request
        :param allowed_roles: allowed user roles.
        :param resource: resource being requested.
        :param method: HTTP method being executed (POST, GET, etc.)
        """
        raise NotImplementedError

    def authenticate(self):
        """ Returns a standard a 401 response that enables basic auth.
        Override if you want to change the response and/or the realm.
        """
        resp = Response(None, 401, {'WWW-Authenticate': 'Basic realm="%s"' %
                                    __package__})
        abort(401, description='Please provide proper credentials',
              response=resp)

    def authorized(self, allowed_roles, resource, method):
        """ Validates the the current request is allowed to pass through.

        :param allowed_roles: allowed roles for the current request, can be a
                              string or a list of roles.
        :param resource: resource being requested.
        """
        auth = request.authorization
        return auth and self.check_auth(auth.username, auth.password,
                                        allowed_roles, resource, method)


class HMACAuth(BasicAuth):
    """ Hash Message Authentication Code (HMAC) authentication logic. Must be
    subclassed to implement custom authorization checking.

    .. versionchanged:: 0.4
       Ensure all errors returns a parseable body #366.

    .. versionchanged:: 0.0.9
       Replaced the now deprecated request.data with request.get_data().

    .. versionchanged:: 0.0.7
       Support for 'resource' argument.

    .. versionadded:: 0.0.5
    """
    def check_auth(self, userid, hmac_hash, headers, data, allowed_roles,
                   resource, method):
        """ This function is called to check if a token is valid. Must be
        overridden with custom logic.

        :param userid: user id included with the request.
        :param hmac_hash: hash included with the request.
        :param headers: request headers. Suitable for hash computing.
        :param data: request data. Suitable for hash computing.
        :param allowed_roles: allowed user roles.
        :param resource: resource being requested.
        :param method: HTTP method being executed (POST, GET, etc.)
        """
        raise NotImplementedError

    def authenticate(self):
        """ Returns a standard a 401. Override if you want to change the
        response.
        """
        abort(401, description='Please provide proper credentials')

    def authorized(self, allowed_roles, resource, method):
        """ Validates the the current request is allowed to pass through.

        :param allowed_roles: allowed roles for the current request, can be a
                              string or a list of roles.
        :param resource: resource being requested.
        """
        auth = request.headers.get('Authorization')
        try:
            userid, hmac_hash = auth.split(':')
        except:
            auth = None
        return auth and self.check_auth(userid, hmac_hash, request.headers,
                                        request.get_data(), allowed_roles,
                                        resource, method)


class TokenAuth(BasicAuth):
    """ Implements Token AUTH logic. Should be subclassed to implement custom
    authentication checking.

    .. versionchanged:: 0.4
       Ensure all errors returns a parseable body #366.

    .. versionchanged:: 0.0.7
       Support for 'resource' argument.

    .. versionadded:: 0.0.5
    """
    def check_auth(self, token, allowed_roles, resource, method):
        """ This function is called to check if a token is valid. Must be
        overridden with custom logic.

        :param token: decoded user name.
        :param allowed_roles: allowed user roles
        :param resource: resource being requested.
        :param method: HTTP method being executed (POST, GET, etc.)
        """
        raise NotImplementedError

    def authenticate(self):
        """ Returns a standard a 401. Override if you want to change the
        response.
        """
        resp = Response(None, 401, {'WWW-Authenticate': 'Basic realm="%s"' %
                                    __package__})
        abort(401, description='Please provide proper credentials',
              response=resp)

    def authorized(self, allowed_roles, resource, method):
        """ Validates the the current request is allowed to pass through.

        :param allowed_roles: allowed roles for the current request, can be a
                              string or a list of roles.
        :param resource: resource being requested.
        """
        auth = None
        if hasattr(request.authorization, 'username'):
            auth = request.authorization.username

        # Werkzeug parse_authorization does not handle
        # "Authorization: <token>" or
        # "Authorization: Token <token>" or
        # "Authorization: Bearer <token>"
        # headers, therefore they should be explicitly handled
        if not auth and request.headers.get('Authorization'):
            auth = request.headers.get('Authorization').strip()
            if auth.lower().startswith(('token', 'bearer')):
                auth = auth.split(' ')[1]

        return auth and self.check_auth(auth, allowed_roles, resource,
                                        method)


def auth_field_and_value(resource):
    """ If auth is active and the resource requires it, return both the
    current request 'request_auth_value' and the 'auth_field' for the resource

    .. versionchanged:: 0.4
       Use new auth.request_auth_value() method.

    .. versionadded:: 0.3
    """
    if '|resource' in request.endpoint:
        # We are on a resource endpoint and need to check against
        # `public_methods`
        public_method_list_to_check = 'public_methods'
    else:
        # We are on an item endpoint and need to check against
        # `public_item_methods`
        public_method_list_to_check = 'public_item_methods'

    resource_dict = app.config['DOMAIN'][resource]
    auth = resource_auth(resource)

    request_auth_value = auth.get_request_auth_value() if auth else None
    auth_field = resource_dict.get('auth_field', None) if request.method not \
        in resource_dict[public_method_list_to_check] else None

    return auth_field, request_auth_value


def resource_auth(resource):
    """ Ensure resource auth is an instance and its state is preserved between
    calls.

    .. versionchanged:: 0.6
       Change name so it can be clearly imported from other modules.

    .. versionadded:: 0.5.2
    """
    resource_def = app.config['DOMAIN'][resource]
    if callable(resource_def['authentication']):
        resource_def['authentication'] = resource_def['authentication']()
    return resource_def['authentication']
