Persevere‘s security system provides a powerful infrastructure for controlling access to a system by combining the best aspects of capability-based security with role-based security. Persevere has a full user management system and granular per-object access control with inheritance. This system is designed such that it is very easy to use the default Persevere security to handle your access or integrate with an existing authentication system. Any part of the security system can be redefined and customized. Persevere can also be started without enabling security to make it easier to start development.
Creating Users in Persevere
Out of the box, Persevere’s security system is disabled and there are restrictions on accessing and modifying data in Persevere. Enabling security is simply done by creating a new user. The first user will be given the super-user role in Persevere, with full access to the system. You can create a new user from the database explorer (explorer.html
in your local install) by clicking on the “Sign In” button and then “Register” (you can alternately create a new user by selecting “User” from the class/table list and clicking “Create New User”). This first user will be the super-user. You can continue to add more users, and by default, subsequent users will have a read-only view of the system unless they are granted further permission. New users can also be created programmatically using the createUser
method on the User class. This is the default user management system for Persevere. Later, we will discuss customized integration with existing user management systems.
Authenticating Users
User authentication can be done through an RPC call to the User class’s authenticate
method. A successful login through this method will create a cookie-based authentication. For example:
POST /Class/User
{
"method":"authentication",
"params: ["username", "password"],
"id":"call1"
}
Alternately, you can authenticate users using standard HTTP Basic Authentication (RFC 2617). This is done by including an Authorization
header with the username and password with base64 encoding. This authentication technique does not create a cookie and the Authorization
header must be sent with each request (that needs authorization). For example:
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Grant Access with a Capability
Persevere’s default mechanism for controlling access to objects is through Capability (capitalized because it is a class in Persevere) objects. A Capability object defines a set of members that are authorized through the Capability and it defines a set of resources that are authorized at different access levels. A Capability fulfills the role of a group (containing a set of users or other Capability/groups) and an ACL (defining access levels for resources). The easiest way to create a new Capability is from the database explorer by clicking on “Grant Access”. The explorer will prompt you for who you want to grant access to and what level of access to grant to the selected resource in the grid (if nothing is selected in the grid, then the table is the target resource). This action will create a new Capability with one member (the indicated user) and one authorized resource (with the indicated access level). This Capability may look like:
{
"members":[{"$ref":"/User/2"}],
"full":[{"$ref":"/SomeClass/4"}]
}
We could modify this Capability to include additional users and grant read-only access to all the objects in the OtherClass
table:
{
"members": [{"$ref": "/User/2"}, {"$ref": "/User/3"}],
"full": [{"$ref": "/SomeClass/4"}],
"read": [{"$ref": "/OtherClass/"}]
}
Unauthorized users are represented by null
, so if we wanted to allow unauthenticated users (the public) access through this Capability, we could do so by adding null
to the members
array. A list of all the access levels is available in the security documentation.
Persevere uses inheritance to determine access levels for objects. When a user attempts to access a class instance, Persevere will first check to see if there is a Capability defined for that object. If it is not, it will look to see if there is a Capability defined for the table, next it will look for the class definition object, and finally it will look at the class definition for Class (the object /Class/Class, which can be used to define the default access level of the whole system). When the first resource in the inheritance chain that has a Capability defined is found, the corresponding Capability will be used to determine the access level.
When the first user is added to Persevere, Persevere will create two default Capability objects. One Capability will be defined for the newly created user to have full access to the entire system (full access to /Class/Class
), and another Capability will be defined to give the public execute
access to the system (they can read and execute publicly accessible methods).
The Sandbox Model
Persevere provides security at a well-defined layer to differentiate between privileged and unprivileged operations. All requests from a remote client (HTTP requests) are checked against the access control system. This means all queries, RPCs, and object modification requests are subject to access limitations. The primary access levels (from lowest to highest and higher permission has all the access of the lower level):
- read – permission to make queries and request data (GET requests)
- execute – permission to execute methods (via JSON-RPC)
- append – permission to create new objects (POST requests)
- write – permission to modify data (PUT requests)
- full – permission to delete objects (DELETE requests)
JavaScript code on the server runs in privileged mode, that is, it can modify data regardless of what user is logged in. Once a RPC is executed, the method can make it’s own decisions about how and what modifications to make. Server side code can use the getAccessLevel
and hasAccessLevel
functions to query for the access levels of the current user. For example, one could write a method for a BlogPost class that will add a comment to a blog if the user has read permission to the post:
"schema":{
"prototype":{
"addComment": function(comment){
if(hasAccessLevel(this, "read")){
this.comments.push(comment);
}
},
...
Because JavaScript on the server runs in privileged mode, you must have full access to the system (super-user) to add methods to classes and objects through HTTP.
Custom Authentication/Integrating with Existing Security System
Persevere may be used in conjunction with an existing user directory system, in which case it may be desirable to define a custom authentication mechanism to integrate with the existing system. This can be done by simply defining a top-level authenticate
function in a JavaScript library module. JavaScript modules are .js files that are placed in the WEB-INF/jslib directory in Persevere. The authenticate function is passed two arguments, the username and password. An example of an authenticate function that would interact with another system, such as an LDAP server, might look like:
authenticate = function(username, password){
if(username == null){
//a null username indicates that the user is signing out
return null;
}
// if we defined a function that handles the credentials check:
var successful = checkCredentials(username,password);
if(!successful){
// this is how we indicate a login failure
throw new AccessError("Authentication failed");
}
// return a user object
return username;
}
The authenticate function can return any object or value that we want to represent the user. The authenticate function could simply return the username as a string, it can pull a User object from Persevere’s User table, or it could return an object from a custom table.
Custom Access Handling
With Persevere you can also define custom access handler functions. This is done by defining a top level getAccessLevel
function that behaves as described in the server JavaScript API documentation for Persevere. If we wanted to define our access scheme such that all the objects in the MyClass
table are fully accessible to everyone, but the rest of the data in Persevere is only accessible to the user with the name “john”, we could define a getAccessLevel
function:
getAccessLevel = function(resource){
if(resource instanceof MyClass){
return 6; // indicates full permission
}
var user = getCurrentUser();
if(user && user.name == 'john'){
return 6; // john has full permission to everything
}
return 3; // everyone else has execute/read permission
}
Security in Persevere
There are other aspects of Persevere’s security system that are automatic. Persevere handles cookie-based authenticated sessions, and also provides comprehensive and robust cross-site request forgery protection. With the powerful role-based capability access control system in Persevere, you can easily create secure applications without needing to create your user management and access control system, yet Persevere still provides the flexibility of customized authentication and authorization schemes.