The security infrastructure in Morepath helps you make sure that web resources published by your application are only accessible by those persons that are allowed to do so. If a person is not allowed access, they will get an appropriate HTTP error: HTTP Forbidden 403.
Before we can determine who is allowed to do what, we need to be able to identify who people are in the first place.
The identity policy in Morepath takes a HTTP request and establishes a
claimed identity for it. For basic authentication for instance it will
extract the username and password. The claimed identity can be
accessed by looking at the
on the request object.
You use the
morepath.App.identity_policy() directive to install
an identity policy into a Morepath app:
from morepath.security import BasicAuthIdentityPolicy @App.identity_policy() def get_identity_policy(): return BasicAuthIdentityPolicy()
If you want to create your own identity policy, see the
morepath.security.IdentityPolicy API documentation to see
what methods you need to implement.
The identity policy only establishes who someone is claimed to be. It doesn’t verify whether that person is actually who they say they are. For identity policies where the browser repeatedly sends the username/password combination to the server, such as with basic authentication and cookie-based authentication, we need to check each time whether the claimed identity is actually a real identity.
By default, Morepath will reject any claimed identities. To let your
application verify identities, you need to use
@App.verify_identity() def verify_identity(identity): return user_has_password(identity.username, identity.password)
identity object received here is as established by the
identity policy. What the attributes of the identity object are
username) is also determined by the specific identity
policy you install.
user_has_password stands in for whatever method you use
to check a user’s password; it’s not part of Morepath.
Session or ticket identity verification¶
If you use an identity policy based on the session (which you’ve made secure otherwise), or on a cryptographic ticket based authentication system such as the one implemented by mod_auth_tkt, the claimed identity is actually enough.
We know that the claimed identity is actually the one given to the
user earlier when they logged in. No database-based identity check is
required to establish that this is a legitimate identity. You can
verify_identity like this:
@App.verify_identity() def verify_identity(identity): # trust the identity established by the identity policy return True
Login and logout¶
So now we know how identity gets established, and how it can be verified. We haven’t discussed yet how a user actually logs in to establish an identity in the first place.
For this, we need two things:
- Some kind of login form. Could be taken care of by client-side code or by a server-side view. We leave this as an exercise for the reader.
- The view that the login data is submitted to when the user tries to log in.
How this works in detail is up to your application. What’s common to login systems is the action we take when the user logs in, and the action we take when the user logs in. When the user logs in we need to remember their identity on the response, and when the user logs in we need to forget their identity again.
Here is a sketch of how logging in works. Imagine we’re in a Morepath
view where we’ve already retrieved
the request (coming from a login form):
# check whether user has password, using password hash and database if not user_has_password(username, password): return "Sorry, login failed" # or something more fancy # now that we've established the user, remember it on the response @request.after def remember(response): identity = morepath.Identity(username) morepath.remember_identity(response, request, identity)
This is enough for session-based or cryptographic ticket-based authentication.
For cookie-based authentication where the password is sent as a cookie
to the server for each request, we need to make sure include the
password the user used to log in, so that
remember can then place
it in the cookie so that it can be sent back to the server:
@request.after def remember(response): identity = morepath.Identity(username, password=password) morepath.remember_identity(response, request, identity)
When you construct the identity using
can any data you want in the identity object by using keyword
Logging out is easy to implement and will work for any kind of
authentication except for basic auth (see later). You simply call
morepath.forget_identity somewhere in the logout view:
@request.after def forget(response): morepath.forget_identity(response, request)
This will cause the login information (in cookie-form) to be removed from the response.
Basic authentication is special in a number of ways:
- The HTTP response status that triggers basic auth is Unauthorized (401), not the default Forbidden (403). This needs to be sent back to the browser each time login fails, so that the browser asks the user for a username and a password.
- The username and password combination is sent to the server by the
browser automatically; there is no need to set some type of cookie
on the response. Therefore
- With basic auth, there is no universal way for a web application to
trigger a log out. Therefore
forget_identitydoes nothing either.
To trigger a
401 status when time Morepath raises a
we can use an exception view, something like this:
from webob.exc import HTTPForbidden @App.view(model=HTTPForbidden) def make_unauthorized(self, request): @request.after def set_status_code(response): response.status_code = 401 return "Unauthorized"
The core of the login code can remain the same as
a no-op, but you could reduce it to this:
# check whether user has password, using password hash and database if not user_has_password(username, password): return "Sorry, login failed" # or something more fancy
Now that we have a way to establish identity and a way for the user to log in, we can move on to permissions. Permissions are per view. You can define rules for your application that determine when a user has a permission.
Let’s say we want two permissions in our application, view and edit. We define those as plain Python classes:
class ViewPermission(object): pass class EditPermission(object): pass
Now we can protect views with those permissions. Let’s say we have a
Document model that we can view and edit:
@App.html(model=Document, permission=ViewPermission) def document_view(request, model): return "<p>The title is: %s</p>" % model.title @App.html(model=Document, name='edit', permission=EditPermission) def document_edit(request, model): return "some kind of edit form"
- Only allow access to
document_viewif the identity has
- Only allow allow access to
document_editif the identity has
Now that we give people a claimed identity and we have guarded our
views with permissions, we need to establish who has what permissions
where using some rules. We can use the
morepath.App.permission_rule() directive to do that.
This is very flexible. Let’s look at some examples.
Let’s give absolutely everybody view permission on
@App.permission_rule(model=Document, permission=ViewPermission) def document_view_permission(identity, model, permission) return True
Let’s give only those users that are in a list
Document the edit permission:
@App.permission_rule(model=Document, permission=EditPermission) def document_edit_permission(identity, model, permission): return identity.userid in model.allowed_users
This is just is one hypothetical rule.
Document objects is totally made up and not part of Morepath. Your
application can have any rule at all, using any data, to determine
whether someone has a permission.
Morepath Super Powers Go!¶
What if we don’t want to have to define permissions on a per-model basis? In our application, we may have a generic way to check for the edit permission on any kind of model. We can easily do that too, as Morepath knows about inheritance:
@App.permission_rule(model=object, permission=EditPermission) def has_edit_permission(identity, model, permission): ... some generic rule ...
This permission function is registered for model
object, so will
be valid for all models in our application.
What if we want that policy for all models, except
we want to do something else? We can do that too:
@App.permission_rule(model=Document, permission=EditPermission) def document_edit_permission(identity, model, permission): ... some special rule ...
You can also register special rules that depend on identity. If you
identity=None, you can can register a permission policy for
when the user has not logged in yet and has no claimed identity:
@App.permission_rule(model=object, permission=EditPermission, identity=None) def has_edit_permission_not_logged_in(identity, model, permission): return False
This permission check works in addition to the ones we specified above.
If you want to defer to a completely generic permission engine, you could define a permission check that works for any permission:
@App.permission_rule(model=object, permission=object) def generic_permission_check(identity, model, permission): ... generic rule ...