Python Pyramid - Security



Pyramid's declarative security system determines the identity of the current user and verifies if the user has access to certain resources. The security policy can prevent the user from invoking a view. Before any view is invoked, the authorization system uses the credentials in the request to determine if access will be allowed.

The security policy is defined as a class that controls the user access with the help of following methods defined in pyramid.security module −

  • forget(request) − This method returns header tuples suitable for 'forgetting' the set of credentials possessed by the currently authenticated user. It is generally used within the body of a view function.

  • remember(request, userid) − This method returns a sequence of header tuples on the request's response. They are suitable for 'remembering' a set of credentials such as userid using the current security policy. Common usage might look like so within the body of a view function.

The authenticated user's access is controlled by the objects of Allowed and Denied classes in this module.

To implement the functionality of identity, remember and forget mechanism, Pyramid provides the following helper classes defined in the pyramid.authentication module −

  • SessionAuthenticationHelper − Store the userid in the session.

  • AuthTktCookieHelper − Store the userid with an "auth ticket" cookie.

We can also use extract_http_basic_credentials() function to retrieve user credentials using HTTP Basic Auth.

To retrieve the userid from REMOTE_USER in the WSGI environment, the request.environ.get('REMOTE_USER') can be used.

Example

Let us now learn how to implement the security policy with the help of following example. The "development.ini" for this example is as follows −

[app:main]
use = egg:tutorial
pyramid.reload_templates = true
pyramid.includes = pyramid_debugtoolbar
hello.secret = a12b

[server:main]
use = egg:waitress#main
listen = localhost:6543

We then write the security policy class in the following Python code saved as security.py

from pyramid.authentication import AuthTktCookieHelper
USERS = {'admin': 'admin', 'manager': 'manager'}
class SecurityPolicy:
   def __init__(self, secret):
      self.authtkt = AuthTktCookieHelper(secret=secret)
   def identity(self, request):
      identity = self.authtkt.identify(request)
      if identity is not None and identity['userid'] in USERS:
      return identity
   def authenticated_userid(self, request):
      identity = self.identity(request)
      if identity is not None:
         return identity['userid']
   def remember(self, request, userid, **kw):
      return self.authtkt.remember(request, userid, **kw)
   def forget(self, request, **kw):
      return self.authtkt.forget(request, **kw)

The __init__.py file in our package folder defines following configuration. The security policy class defined above is added in the configuration with set_security_policy() method of Configurator class. Three routes - home, login and logout – are added to the configuration.

from pyramid.config import Configurator
from .security import SecurityPolicy

def main(global_config, **settings):
   config = Configurator(settings=settings)
   config.include('pyramid_chameleon')
   config.set_security_policy(
      SecurityPolicy(
         secret=settings['hello.secret'],
      ),
   )
   config.add_route('home', '/')
   config.add_route('login', '/login')
   config.add_route('logout', '/logout')
   config.scan('.views')
   return config.make_wsgi_app()

Three views corresponding to the above routes are defined in views.py.

from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget

from pyramid.view import view_config, view_defaults
from .security import USERS

@view_defaults(renderer='home.pt')
class HelloViews:
   def __init__(self, request):
      self.request = request
      self.logged_in = request.authenticated_userid
   @view_config(route_name='home')
   def home(self):
      return {'name': 'Welcome'}
   @view_config(route_name='login', renderer='login.pt')
   def login(self):
      request = self.request
      login_url = request.route_url('login')
      referrer = request.url
      if referrer == login_url:
         referrer = '/'
      came_from = request.params.get('came_from', referrer)
      message = ''
      login = ''
      password = ''
      if 'form.submitted' in request.params:
         login = request.params['login']
         password = request.params['password']
         pw = USERS.get(login)
         if pw == password:
            headers = remember(request, login)
            return HTTPFound(location=came_from, headers=headers)
         message = 'Failed login'
         return dict(
            name='Login', message=message,
            url=request.application_url + '/login',
            came_from=came_from,
            login=login, password=password,)
            
   @view_config(route_name='logout')
   def logout(self):
      request = self.request
      headers = forget(request)
      url = request.route_url('home')
      return HTTPFound(location=url, headers=headers)

The login view renders the login form. When the user Id and password entered by the user are verified against the list of USERS, the details are 'remembered'. On the other hand, the logout view releases these details by 'forgetting'.

The home view renders the following chameleon template - home.pt

<!DOCTYPE html>
<html lang="en">
<body>
   <div>
      <a tal:condition="view.logged_in is None" href="${request.application_url}/login">Log In</a>
      <a tal:condition="view.logged_in is not None" href="${request.application_url}/logout">Logout</a>
   </div>
   <h1>Hello. ${name}</h1>
</body>
</html>

Following is the chameleon template login.pt for login view.

<!DOCTYPE html>
<html lang="en">
<body>
   <h1>Login</h1>
   <span tal:replace="message"/>

   <form action="${url}" method="post">
      <input type="hidden" name="came_from" value="${came_from}"/>
      <label for="login">Username</label>
      <input type="text" id="login" name="login" value="${login}"/><br/>
      <label for="password">Password</label>
      <input type="password" id="password" name="password" value="${password}"/><br/>
      <input type="submit" name="form.submitted" value="Log In"/>
   </form>
</body>
</html>

The development.ini and setup.py are placed in the outer project folder, while, the __init__.py, views.py, security.py and the templates home.pt as well as login.pt should be saved under the package folder named hello.

Install the package with the following command −

Env\hello>pip3 install -e.

Start the server with the pserve utility.

pserve development.ini

Output

Open the browser and visit http://localhost:6543/ link.

Welcome

Click the "Log In" link to open the login form −

Login

The home view page comes back with the link changed to logout as the credentials are remembered.

Hello

Clicking the "logout" link will result in forgetting the credentials and the default home page will be shown.

Advertisements