Security¶
Introduction¶
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.
Identity¶
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 morepath.Request.identity
attribute
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.
Verify identity¶
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
morepath.App.verify_identity()
:
@App.verify_identity()
def verify_identity(identity):
return user_has_password(identity.username, identity.password)
The identity
object received here is as established by the
identity policy. What the attributes of the identity object are
(besides username
) is also determined by the specific identity
policy you install.
Note that 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
therefore implement 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 username
and password
from
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 morepath.security.Identity
, you
can any data you want in the identity object by using keyword
parameters.
Logging out¶
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¶
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
remember_identity
does nothing. - With basic auth, there is no universal way for a web application to
trigger a log out. Therefore
forget_identity
does nothing either.
To trigger a 401
status when time Morepath raises a 403
status,
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 remember_identity
is
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
Permissions¶
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"
This says:
- Only allow access to
document_view
if the identity hasViewPermission
on theDocument
model. - Only allow allow access to
document_edit
if the identity hasEditPermission
on theDocument
model.
Permission rules¶
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 Document
:
@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 allowed_users
on
the 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. allowed_users
on
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 Document
where
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
pass 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 ...