Reputation: 2314
I've been looking for a way to authenticate user by user and password passed in http header.
curl --user user1:pass1 http://localhost:6543/the_resource
The idea is to check if passed credentials allow user to view *the_resource* and if not return 401 - Forbidden.
I've found only examples of authentication policy where there has to be a login and logout view or this basic authentication policy which I don't know how to bind with Pyramid's ACL.
I will appreciate any help, how to start.
One more thing came to my mind. How to force this pup-up login window for basic authentication?
Upvotes: 4
Views: 3528
Reputation: 2314
In the end it became clear how to use authentication and authorization. Everything was actually written I just didn't catch the concept at once. I'll try to write how I got it working explaining in a noobish way, which I had to explain it to myself. I hope it will be useful to someone. Sources in the end may help to understand my writing ;) All comments are welcome. If I got something wrong, please correct me.
Most important is the basic authentication in which BasicAuthenticationPolicy must have methods that can be used later in pyramid application - like authenticated_userid(request). These methods use _get_basicauth_credentials() which pulls out login and password that were passed in http header. The actual checking if the login and password are correct happens in mycheck().
Now in __init__.py we must add BasicAuthenticationPolicy with method mycheck as an argument to our application configurator, so the pyramid can use it.
In a matter of authentication that is all. Now you should be able if and who was authenticated using authenticated_userid(request) (see views.py)
To use pyramid authorization to resources we need to add ACLAuthorizationPolicy to our configurator in __init__.py and add __acl__ to the resources. In most simple case to the root_factory (see this and this) ACL defines which group has what permission. If I'm not mistaken in (Allow, 'group:viewers', 'view') 'group:viewers' has to be what authentication method - mycheck() - returns.
The last step in authorization is add permission to certain view using a decorator (or in add_route). If we add the ACL permission - view - to a view_page then group:viewers is allowed to see that page (call view_page).
basic_authentication.py
import binascii
from zope.interface import implements
from paste.httpheaders import AUTHORIZATION
from paste.httpheaders import WWW_AUTHENTICATE
from pyramid.interfaces import IAuthenticationPolicy
from pyramid.security import Everyone
from pyramid.security import Authenticated
import yaml
def mycheck(credentials, request):
login = credentials['login']
password = credentials['password']
USERS = {'user1':'pass1',
'user2':'pass2'}
GROUPS = {'user1':['group:viewers'],
'user2':['group:editors']}
if login in USERS and USERS[login] == password:
return GROUPS.get(login, [])
else:
return None
def _get_basicauth_credentials(request):
authorization = AUTHORIZATION(request.environ)
try:
authmeth, auth = authorization.split(' ', 1)
except ValueError: # not enough values to unpack
return None
if authmeth.lower() == 'basic':
try:
auth = auth.strip().decode('base64')
except binascii.Error: # can't decode
return None
try:
login, password = auth.split(':', 1)
except ValueError: # not enough values to unpack
return None
return {'login':login, 'password':password}
return None
class BasicAuthenticationPolicy(object):
""" A :app:`Pyramid` :term:`authentication policy` which
obtains data from basic authentication headers.
Constructor Arguments
``check``
A callback passed the credentials and the request,
expected to return None if the userid doesn't exist or a sequence
of group identifiers (possibly empty) if the user does exist.
Required.
``realm``
Default: ``Realm``. The Basic Auth realm string.
"""
implements(IAuthenticationPolicy)
def __init__(self, check, realm='Realm'):
self.check = check
self.realm = realm
def authenticated_userid(self, request):
credentials = _get_basicauth_credentials(request)
if credentials is None:
return None
userid = credentials['login']
if self.check(credentials, request) is not None: # is not None!
return userid
def effective_principals(self, request):
effective_principals = [Everyone]
credentials = _get_basicauth_credentials(request)
if credentials is None:
return effective_principals
userid = credentials['login']
groups = self.check(credentials, request)
if groups is None: # is None!
return effective_principals
effective_principals.append(Authenticated)
effective_principals.append(userid)
effective_principals.extend(groups)
return effective_principals
def unauthenticated_userid(self, request):
creds = self._get_credentials(request)
if creds is not None:
return creds['login']
return None
def remember(self, request, principal, **kw):
return []
def forget(self, request):
head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
return head
myproject.__init__.py
from pyramid.config import Configurator
from myproject.resources import Root
from myproject.basic_authentication import BasicAuthenticationPolicy, mycheck
from pyramid.authorization import ACLAuthorizationPolicy
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(root_factory='myproject.models.RootFactory',
settings=settings,
authentication_policy=BasicAuthenticationPolicy(mycheck),
authorization_policy=ACLAuthorizationPolicy(),
)
config.add_static_view('static', 'myproject:static', cache_max_age=3600)
config.add_route('view_page', '/view')
config.add_route('edit_page', '/edit')
config.scan()
app = config.make_wsgi_app()
return app
models.py
from pyramid.security import Allow
class RootFactory(object):
__acl__ = [ (Allow, 'group:viewers', 'view'),
(Allow, 'group:editors', 'edit') ]
def __init__(self, request):
pass
views.py
from pyramid.security import authenticated_userid
from pyramid.view import view_config
#def my_view(request):
# return render_to_response('templates/simple.pt', {})
@view_config(route_name='view_page', renderer='templates/view.pt', permission='view')
def view_page(request):
return {}
@view_config(route_name='edit_page', renderer='templates/edit.pt', permission='edit')
def edit_page(request):
return {}
Upvotes: 12
Reputation: 23331
Well what you are asking for is Basic auth. You linked to the recipe, which you'll want to use. This handles identifying users and computing their principals. Principals are used by the ACL system, along with the permission specified on the view, to determine Allow/Deny access.
I think the trick is figuring out how to handle denying a user access to the resource, which is not enumerated in that recipe. You can do that by providing a custom "Forbidden View" that is invoked on a URL when the view denies access. At which point, Basic states that you provide a challenge to the client.
@forbidden_view_config()
def forbidden_view(request):
resp = HTTPUnauthorized()
resp.www_authenticate = 'Basic realm="Secure Area"'
return resp
That's untested, but gives you the general idea of how to use a forbidden view. This will challenge the client, and then they are free to make another request (hopefully with credentials) that gets turned into principals that are mapped to the permissions you care about.
Upvotes: 3