from zope.deprecation import deprecated
from zope.interface import providedBy

from pyramid.interfaces import (
    IAuthenticationPolicy,
    IAuthorizationPolicy,
    ISecuredView,
    IView,
    IViewClassifier,
    )

from pyramid.compat import map_
from pyramid.threadlocal import get_current_registry

Everyone = 'system.Everyone'
Authenticated = 'system.Authenticated'
Allow = 'Allow'
Deny = 'Deny'

_marker = object()

class AllPermissionsList(object):
    """ Stand in 'permission list' to represent all permissions """

    def __iter__(self):
        return iter(())

    def __contains__(self, other):
        return True

    def __eq__(self, other):
        return isinstance(other, self.__class__)

ALL_PERMISSIONS = AllPermissionsList()
DENY_ALL = (Deny, Everyone, ALL_PERMISSIONS)

NO_PERMISSION_REQUIRED = '__no_permission_required__'

def _get_registry(request):
    try:
        reg = request.registry
    except AttributeError:
        reg = get_current_registry() # b/c
    return reg

def _get_authentication_policy(request):
    registry = _get_registry(request)
    return registry.queryUtility(IAuthenticationPolicy)

def has_permission(permission, context, request):
    """
    A function that calls :meth:`pyramid.request.Request.has_permission`
    and returns its result.
    
    .. deprecated:: 1.5
        Use :meth:`pyramid.request.Request.has_permission` instead.

    .. versionchanged:: 1.5a3
        If context is None, then attempt to use the context attribute of self;
        if not set, then the AttributeError is propagated.
    """    
    return request.has_permission(permission, context)

deprecated(
    'has_permission',
    'As of Pyramid 1.5 the "pyramid.security.has_permission" API is now '
    'deprecated.  It will be removed in Pyramid 1.8.  Use the '
    '"has_permission" method of the Pyramid request instead.'
    )


def authenticated_userid(request):
    """
    A function that returns the value of the property
    :attr:`pyramid.request.Request.authenticated_userid`.
    
    .. deprecated:: 1.5
       Use :attr:`pyramid.request.Request.authenticated_userid` instead.
    """        
    return request.authenticated_userid

deprecated(
    'authenticated_userid',
    'As of Pyramid 1.5 the "pyramid.security.authenticated_userid" API is now '
    'deprecated.  It will be removed in Pyramid 1.8.  Use the '
    '"authenticated_userid" attribute of the Pyramid request instead.'
    )

def unauthenticated_userid(request):
    """ 
    A function that returns the value of the property
    :attr:`pyramid.request.Request.unauthenticated_userid`.
    
    .. deprecated:: 1.5
        Use :attr:`pyramid.request.Request.unauthenticated_userid` instead.
    """        
    return request.unauthenticated_userid

deprecated(
    'unauthenticated_userid',
    'As of Pyramid 1.5 the "pyramid.security.unauthenticated_userid" API is '
    'now deprecated.  It will be removed in Pyramid 1.8.  Use the '
    '"unauthenticated_userid" attribute of the Pyramid request instead.'
    )

def effective_principals(request):
    """
    A function that returns the value of the property
    :attr:`pyramid.request.Request.effective_principals`.
    
    .. deprecated:: 1.5
        Use :attr:`pyramid.request.Request.effective_principals` instead.
    """            
    return request.effective_principals

deprecated(
    'effective_principals',
    'As of Pyramid 1.5 the "pyramid.security.effective_principals" API is '
    'now deprecated.  It will be removed in Pyramid 1.8.  Use the '
    '"effective_principals" attribute of the Pyramid request instead.'
    )

def remember(request, userid=_marker, **kw):
    """
    Returns a sequence of header tuples (e.g. ``[('Set-Cookie', 'foo=abc')]``)
    on this request's response.
    These headers are suitable for 'remembering' a set of credentials
    implied by the data passed as ``userid`` and ``*kw`` using the
    current :term:`authentication policy`.  Common usage might look
    like so within the body of a view function (``response`` is
    assumed to be a :term:`WebOb` -style :term:`response` object
    computed previously by the view code):

    .. code-block:: python

       from pyramid.security import remember
       headers = remember(request, 'chrism', password='123', max_age='86400')
       response = request.response
       response.headerlist.extend(headers)
       return response

    If no :term:`authentication policy` is in use, this function will
    always return an empty sequence. If used, the composition and
    meaning of ``**kw`` must be agreed upon by the calling code and
    the effective authentication policy.
    
    .. deprecated:: 1.6
        Renamed the ``principal`` argument to ``userid`` to clarify its
        purpose.
    """
    if userid is _marker:
        principal = kw.pop('principal', _marker)
        if principal is _marker:
            raise TypeError(
                'remember() missing 1 required positional argument: '
                '\'userid\'')
        else:
            deprecated(
                'principal',
                'The "principal" argument was deprecated in Pyramid 1.6. '
                'It will be removed in Pyramid 1.9. Use the "userid" '
                'argument instead.')
            userid = principal
    policy = _get_authentication_policy(request)
    if policy is None:
        return []
    return policy.remember(request, userid, **kw)

def forget(request):
    """
    Return a sequence of header tuples (e.g. ``[('Set-Cookie',
    'foo=abc')]``) suitable for 'forgetting' the set of credentials
    possessed by the currently authenticated user.  A common usage
    might look like so within the body of a view function
    (``response`` is assumed to be an :term:`WebOb` -style
    :term:`response` object computed previously by the view code):

    .. code-block:: python

       from pyramid.security import forget
       headers = forget(request)
       response.headerlist.extend(headers)
       return response

    If no :term:`authentication policy` is in use, this function will
    always return an empty sequence.
    """            
    policy = _get_authentication_policy(request)
    if policy is None:
        return []
    return policy.forget(request)

def principals_allowed_by_permission(context, permission):
    """ Provided a ``context`` (a resource object), and a ``permission``
    (a string or unicode object), if a :term:`authorization policy` is
    in effect, return a sequence of :term:`principal` ids that possess
    the permission in the ``context``.  If no authorization policy is
    in effect, this will return a sequence with the single value
    :mod:`pyramid.security.Everyone` (the special principal
    identifier representing all principals).

    .. note::

       even if an :term:`authorization policy` is in effect,
       some (exotic) authorization policies may not implement the
       required machinery for this function; those will cause a
       :exc:`NotImplementedError` exception to be raised when this
       function is invoked.
    """
    reg = get_current_registry()
    policy = reg.queryUtility(IAuthorizationPolicy)
    if policy is None:
        return [Everyone]
    return policy.principals_allowed_by_permission(context, permission)

def view_execution_permitted(context, request, name=''):
    """ If the view specified by ``context`` and ``name`` is protected
    by a :term:`permission`, check the permission associated with the
    view using the effective authentication/authorization policies and
    the ``request``.  Return a boolean result.  If no
    :term:`authorization policy` is in effect, or if the view is not
    protected by a permission, return ``True``. If no view can view found,
    an exception will be raised.

    .. versionchanged:: 1.4a4
       An exception is raised if no view is found.

    """
    reg = _get_registry(request)
    provides = [IViewClassifier] + map_(providedBy, (request, context))
    # XXX not sure what to do here about using _find_views or analogue;
    # for now let's just keep it as-is
    view = reg.adapters.lookup(provides, ISecuredView, name=name)
    if view is None:
        view = reg.adapters.lookup(provides, IView, name=name)
        if view is None:
            raise TypeError('No registered view satisfies the constraints. '
                            'It would not make sense to claim that this view '
                            '"is" or "is not" permitted.')
        return Allowed(
            'Allowed: view name %r in context %r (no permission defined)' %
            (name, context))
    return view.__permitted__(context, request)


class PermitsResult(int):
    def __new__(cls, s, *args):
        """
        Create a new instance.

        :param fmt: A format string explaining the reason for denial.
        :param args: Arguments are stored and used with the format string
                      to generate the ``msg``.

        """
        inst = int.__new__(cls, cls.boolval)
        inst.s = s
        inst.args = args
        return inst

    @property
    def msg(self):
        """ A string indicating why the result was generated."""
        return self.s % self.args

    def __str__(self):
        return self.msg

    def __repr__(self):
        return '<%s instance at %s with msg %r>' % (self.__class__.__name__,
                                                    id(self),
                                                    self.msg)

class Denied(PermitsResult):
    """
    An instance of ``Denied`` is returned when a security-related
    API or other :app:`Pyramid` code denies an action unrelated to
    an ACL check.  It evaluates equal to all boolean false types.  It
    has an attribute named ``msg`` describing the circumstances for
    the deny.

    """
    boolval = 0

class Allowed(PermitsResult):
    """
    An instance of ``Allowed`` is returned when a security-related
    API or other :app:`Pyramid` code allows an action unrelated to
    an ACL check.  It evaluates equal to all boolean true types.  It
    has an attribute named ``msg`` describing the circumstances for
    the allow.

    """
    boolval = 1

class ACLPermitsResult(PermitsResult):
    def __new__(cls, ace, acl, permission, principals, context):
        """
        Create a new instance.

        :param ace: The :term:`ACE` that matched, triggering the result.
        :param acl: The :term:`ACL` containing ``ace``.
        :param permission: The required :term:`permission`.
        :param principals: The list of :term:`principals <principal>` provided.
        :param context: The :term:`context` providing the :term:`lineage`
                        searched.

        """
        fmt = ('%s permission %r via ACE %r in ACL %r on context %r for '
               'principals %r')
        inst = PermitsResult.__new__(
            cls,
            fmt,
            cls.__name__,
            permission,
            ace,
            acl,
            context,
            principals,
        )
        inst.permission = permission
        inst.ace = ace
        inst.acl = acl
        inst.principals = principals
        inst.context = context
        return inst

class ACLDenied(ACLPermitsResult, Denied):
    """
    An instance of ``ACLDenied`` is a specialization of
    :class:`pyramid.security.Denied` that represents that a security check
    made explicitly against ACL was denied.  It evaluates equal to all
    boolean false types.  It also has the following attributes: ``acl``,
    ``ace``, ``permission``, ``principals``, and ``context``.  These
    attributes indicate the security values involved in the request.  Its
    ``__str__`` method prints a summary of these attributes for debugging
    purposes. The same summary is available as the ``msg`` attribute.

    """

class ACLAllowed(ACLPermitsResult, Allowed):
    """
    An instance of ``ACLAllowed`` is a specialization of
    :class:`pyramid.security.Allowed` that represents that a security check
    made explicitly against ACL was allowed.  It evaluates equal to all
    boolean true types.  It also has the following attributes: ``acl``,
    ``ace``, ``permission``, ``principals``, and ``context``.  These
    attributes indicate the security values involved in the request.  Its
    ``__str__`` method prints a summary of these attributes for debugging
    purposes. The same summary is available as the ``msg`` attribute.

    """

class AuthenticationAPIMixin(object):

    def _get_authentication_policy(self):
        reg = _get_registry(self)
        return reg.queryUtility(IAuthenticationPolicy)

    @property
    def authenticated_userid(self):
        """ Return the userid of the currently authenticated user or
        ``None`` if there is no :term:`authentication policy` in effect or
        there is no currently authenticated user.

        .. versionadded:: 1.5
        """
        policy = self._get_authentication_policy()
        if policy is None:
            return None
        return policy.authenticated_userid(self)

    @property
    def unauthenticated_userid(self):
        """ Return an object which represents the *claimed* (not verified) user
        id of the credentials present in the request. ``None`` if there is no
        :term:`authentication policy` in effect or there is no user data
        associated with the current request.  This differs from
        :attr:`~pyramid.request.Request.authenticated_userid`, because the
        effective authentication policy will not ensure that a record
        associated with the userid exists in persistent storage.

        .. versionadded:: 1.5
        """
        policy = self._get_authentication_policy()
        if policy is None:
            return None
        return policy.unauthenticated_userid(self)

    @property
    def effective_principals(self):
        """ Return the list of 'effective' :term:`principal` identifiers
        for the ``request``. If no :term:`authentication policy` is in effect,
        this will return a one-element list containing the
        :data:`pyramid.security.Everyone` principal.

        .. versionadded:: 1.5
        """
        policy = self._get_authentication_policy()
        if policy is None:
            return [Everyone]
        return policy.effective_principals(self)

class AuthorizationAPIMixin(object):

    def has_permission(self, permission, context=None):
        """ Given a permission and an optional context, returns an instance of
        :data:`pyramid.security.Allowed` if the permission is granted to this
        request with the provided context, or the context already associated
        with the request.  Otherwise, returns an instance of
        :data:`pyramid.security.Denied`.  This method delegates to the current
        authentication and authorization policies.  Returns
        :data:`pyramid.security.Allowed` unconditionally if no authentication
        policy has been registered for this request.  If ``context`` is not
        supplied or is supplied as ``None``, the context used is the
        ``request.context`` attribute.

        :param permission: Does this request have the given permission?
        :type permission: unicode, str
        :param context: A resource object or ``None``
        :type context: object
        :returns: Either :class:`pyramid.security.Allowed` or
                  :class:`pyramid.security.Denied`.

        .. versionadded:: 1.5

        """
        if context is None:
            context = self.context
        reg = _get_registry(self)
        authn_policy = reg.queryUtility(IAuthenticationPolicy)
        if authn_policy is None:
            return Allowed('No authentication policy in use.')
        authz_policy = reg.queryUtility(IAuthorizationPolicy)
        if authz_policy is None:
            raise ValueError('Authentication policy registered without '
                             'authorization policy') # should never happen
        principals = authn_policy.effective_principals(self)
        return authz_policy.permits(context, principals, permission)
