-
Notifications
You must be signed in to change notification settings - Fork 887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add pyramid_authstack.AuthenticationStackPolicy as a core pyramid policy #3452
Comments
At the very least, it should be included in the list of add-ons on trypyramid.com. Use this link to create a new issue: |
@ergo why add it to Pyramid directly? That increases the maintenance burden for core devs, adds to our testing requirements, and we have been telling people to use sub-classing of the authentication policy rather than using the callback mechanism as the callback mechanism confuses people and makes the code less readable/understandable than modifying the policy directly. |
I think the case where you want user to authenticate via more than one policy at same time: for example via auth_tkt and/or token/apikey is common enough scenario warrant this functionality provided out of the box. @bertjwregeer Have you seen how small the code - it is 40 LOC, I think it should not be a dependency but part of pyramid itself. I raised the subject of incorporating this as another policy because that would be a non-breaking change. |
We have been telling people that want to write custom authentication policies that they should sub-class one of the existing ones and override Other noticeable issues I see: all of my custom authentication policies have My policies also always return It steps through the sub-policies for Do note that the current example shows
Also, note that if you were to call
For example, would never call And since in the example there is no Overall I don't think it is a good idea to add this to Pyramid core. There are significant pitfalls in the current implementation, and interoperability with other policies is meh at best (Mozilla's multiauth has similar pitfalls). I really like the fact that Pyramid has such a wide range and scope of add-ons that can be pulled from as necessary, and let the eco-system outside of core flourish. For supporting auth_tkt or token/apikey there are different paths that are going to be followed. I would argue you'd want the authentication policy to be a factory that can instantiate the correct one depending on how the request is being made (for example, if Authorization header exists, do api key, if auth tkt cookie exists use authtkt based policy), or have the policy split at a different boundary. For the example in the code, if you store a token server-side as I argue for in my |
You raised some very valid concerns here, thanks for that. Would following policy make more sense then? @implementer(IAuthenticationPolicy)
class PyramidSelectorPolicy(object):
def __init__(self, policy_selector=None, policies=None):
"""
Policy factory - a callable that accepts argument ``request`` and
decides which policy instance should be returned based on that.
That name will be added to request object as an attribute ``matched_auth_policy``.
The factory should always return a policy.
Example usage::
auth_tkt = AuthTktAuthenticationPolicy(...)
auth_token_policy = AuthTokenAuthenticationPolicy(...)
def policy_selector(request):
# default policy
policy = "auth_tkt"
# return API token policy if header is present
if request.headers.get("x-testscaffold-auth-token"):
policy = "auth_token_policy"
log.info("Policy used: {}".format(policy))
return policy
auth_policy = PyramidSelectorPolicy(
policy_selector=policy_selector,
policies={
"auth_tkt": auth_tkt,
"auth_token_policy": auth_token_policy
}
)
Configurator(settings=settings, authentication_policy=auth_policy,...)
:param policy_factory:
"""
self.policy_selector = policy_selector
self.policies = policies
def _get_policy(self, request, policy_key):
if policy_key not in self.policies:
raise ValueError("Policy {} is not found in PyramidSelectorPolicy".format(policy_key))
request.matched_auth_policy = policy_key
return self.policies[policy_key]
def authenticated_userid(self, request):
""" Return the authenticated :term:`userid` or ``None`` if
no authenticated userid can be found. This method of the
policy should ensure that a record exists in whatever
persistent store is used related to the user (the user
should not have been deleted); if a record associated with
the current id does not exist in a persistent store, it
should return ``None``.
"""
policy = self._get_policy(request, self.policy_selector(request))
return policy.authenticated_userid(request)
def unauthenticated_userid(self, request):
""" Return the *unauthenticated* userid. This method
performs the same duty as ``authenticated_userid`` but is
permitted to return the userid based only on data present
in the request; it needn't (and shouldn't) check any
persistent store to ensure that the user record related to
the request userid exists.
This method is intended primarily a helper to assist the
``authenticated_userid`` method in pulling credentials out
of the request data, abstracting away the specific headers,
query strings, etc that are used to authenticate the request.
"""
policy = self._get_policy(request, self.policy_selector(request))
return policy.unauthenticated_userid(request)
def effective_principals(self, request):
""" Return a sequence representing the effective principals
typically including the :term:`userid` and any groups belonged
to by the current user, always including 'system' groups such
as ``pyramid.security.Everyone`` and
``pyramid.security.Authenticated``.
"""
policy = self._get_policy(request, self.policy_selector(request))
return policy.effective_principals(request)
def remember(self, request, userid, **kw):
""" Return a set of headers suitable for 'remembering' the
:term:`userid` named ``userid`` when set in a response. An
individual authentication policy and its consumers can
decide on the composition and meaning of **kw.
"""
policy = self._get_policy(request, self.policy_selector(request))
return policy.remember(request, userid, **kw)
def forget(self, request):
""" Return a set of headers suitable for 'forgetting' the
current user on subsequent requests.
"""
policy = self._get_policy(request, self.policy_selector(request))
return policy.forget(request) |
I wouldn't be opposed to adding something to core, but I want to see the API that shakes out of #3422 (comment) first before even considering adding something new to core. Especially since we are attempting to make the authentication/authorization story much better overall (and thus hopefully simplifying how to do this sort of stuff) |
Sure, makes sense to not double the work. |
@ergo I like it much better than the previous solution. I also believe it to be more correct. |
I'd really like to see an impl of this idea usable with the |
I have a toy app with following policy and it switches between policies without issues. @implementer(ISecurityPolicy)
class PyramidSelectorPolicy(object):
def __init__(self, policy_selector=None, policies=None):
"""
Policy factory - a callable that accepts argument ``request`` and
decides which policy instance should be returned based on that.
That name will be added to request object as an attribute ``matched_security_policy``.
The factory should always return a policy.
Example usage::
auth_tkt = AuthTktAuthenticationPolicy(...)
auth_token_policy = AuthTokenAuthenticationPolicy(...)
def policy_selector(request):
# default policy
policy = "auth_tkt"
# return API token policy if header is present
if request.headers.get("x-auth-token"):
policy = "auth_token_policy"
log.info("Policy used: {}".format(policy))
return policy
security_policy = PyramidSelectorPolicy(
policy_selector=policy_selector,
policies={
"auth_tkt": auth_tkt,
"auth_token_policy": auth_token_policy
}
)
Configurator(settings=settings, security_policy=security_policy,...)
:param policy_factory:
"""
self.policy_selector = policy_selector
self.policies = policies
def _get_policy(self, request, policy_key):
if policy_key not in self.policies:
raise ValueError(
"Policy {} is not found in PyramidSelectorPolicy".format(policy_key)
)
request.matched_security_policy = policy_key
return self.policies[policy_key]
def identify(self, request):
""" Return an object identifying a trusted and verified user. This
object may be anything, but should implement a ``__str__`` method that
outputs a corresponding :term:`userid`.
"""
policy = self._get_policy(request, self.policy_selector(request))
return policy.identify(request)
def permits(self, request, context, identity, permission):
""" Return an instance of :class:`pyramid.security.Allowed` if a user
of the given identity is allowed the ``permission`` in the current
``context``, else return an instance of
:class:`pyramid.security.Denied`.
"""
policy = self._get_policy(request, self.policy_selector(request))
return policy.permits(request, context, identity, permission)
def remember(self, request, userid, **kw):
""" Return a set of headers suitable for 'remembering' the
:term:`userid` named ``userid`` when set in a response. An
individual authentication policy and its consumers can
decide on the composition and meaning of ``**kw``.
"""
policy_key = kw.get("policy")
policy = self._get_policy(request, policy_key)
return policy.remember(request, userid, **kw)
def forget(self, request, **kw):
""" Return a set of headers suitable for 'forgetting' the
current user on subsequent requests, you may pass `policy`
name to specify which identity should be forgotten, otherwise
currently matched identity will be removed if possible.
"""
policy_key = kw.get("policy") or getattr(
request, "matched_security_policy", None
)
if policy_key is not None:
policy = self._get_policy(request, policy_key)
return policy.forget(request) |
Example usage in a view: remember(request, "aaaaaa", policy="policy_a_name")
remember(request, "bbbbb", policy="policy_b_name")
# assumes **kw support in forget()
forget(request) # forget currently matched identity
forget(request, policy="policy_a_name")
forget(request, policy="policy_b_name") So far the one issue I've noticed is that currently pyramid's Adding a
Bonus question - would it make sense to have |
Hi, any thoughts on this? |
I haven't had a chance to take a look at the latest updates to the security policy stuff, but this multiple stack switching looks okay to me. |
IMO, I prefer the authstack implementation of just looping through the policy stack and returning the result of the first hit. A policy_selector seems like an unnecessary knob. Overall, I'm in favor of adding a stacked auth policy to the core. |
@luhn there may be cases where you want to sign out a single device/identity, or looking up identity via first hit could be expensive compared to others, there should be a way to customize the behavior. |
I'm not convinced the value of the knob is worth the increased barrier to entry. Every user would need to implement it, no matter how simple their needs. To make matters worse, in order to properly do that they'd need to understand the implementation details of the policies they're using, to determine which one is "active." Users with simple needs shouldn't have to do more work to satisfy the users with complicated needs. |
You can ship default implementation of |
@luhn I don't think you need to know which policy is currently active for request - it will be available as |
I was referring strictly to policy_selector. To write that function, you need to know the implementation details of the child policies. Can you give an example of how a policy selector could be more efficient than looping through well-implemented child policies? The more I think about it, assuming the child identity methods return as quick as they possibly can, a selector and a loop end up being equivalent. |
@luhn A policy could check database for example (like api key policy - looking up header names/whatever) this is common in multi-tenant environments, a framework should not guess what their users might want to do.
Then a default selector that loops would resolve the problem you raise here - correct? |
I still don't see the need, but as long as there's a sensible default I won't make too much fuss. I'm interested to hear what everybody else thinks. |
Closing this since |
Reference: https://github.com/wichert/pyramid_authstack
I've found it very useful to have multiple authentication methods in my pyramid applications, I always used
pyramid_authstack
for this purpose.After talking with Wichert he was enthusiastic to the idea of making it core pyramid auth.
In my opinion it would make sense to have an out-of-the box option to have stackable policies.
This addition would not require any changes to pyramid tutorial, just documentation of new policy + some mentions in authentication section.
Alternatives include Mozilla's https://github.com/mozilla-services/pyramid_multiauth - however
pyramid_authstack
is a simple implementation that is easy to understand and reason about so I would consider it as the primary candidate for implementation.The text was updated successfully, but these errors were encountered: