BCHS Logo

BCHS

tl;dr kwebapp (as of version 0.3.0) has gained role-based access control (RBAC) facilities allowing the generated data layer to be aware of user roles. This allows developers to sandbox database operations to a particular user role.

BCHS: API provisioning

When deploying our BCHS web applications, we need to evaluate the full stack for its security.

  • Is the physical location secure? (Attack dogs, guns, clowns.)
  • Are discs encrypted? (Software FDE.)
  • Are our file-systems well-partitioned with regards to users? (hier(7), etc.)
  • Are our ingress systems secure? (sshd(8), httpd(8), etc.)
  • Is our application properly privilege separated? (pledge(2), chroot(2), etc.)

All of these account for an application's external environment. But what about the environment internal to the application's run-time—say, making sure that a request servicing a user doesn't try to modify administrative tables?

Enter kwebapp's new API provisioning facility.

What is kwebapp?

kwebapp is a system that uses a configuration file to define the data model of your BCHS application: the data itself (sqlite3), insertion routines, deletion, all sorts of querying, etc. It uses ksql to manipulate the underlying database, and can optionally pull in kcgi to validate HTTP fields for safe entry into the database. A previous article covers the system in detail.

Let's start with a simple example that defines part of a web application: a login session (usually associated with a session cookie) and a user record.

struct user {
  field email email unique;
  field hash password;
  search email, hash: name creds comment 
    "Lookup by credentials.";
  field id int rowid;
};

struct session {
  field user struct userid;
  field userid:user.id int comment 
    "User associated with session.";
  field id int rowid;
  insert;
  search id: name id comment 
    "Lookup by unique identifier.";
  delete id;
};

With this snippet, we've defined struct session and struct user. These are implemented in SQL as tables, with each field being a column. For example, you can see how the session object (and its documentation) carries into generated SQL:

CREATE TABLE session (
  -- User associated with session.
  userid INTEGER NOT NULL,
  id INTEGER PRIMARY KEY,
  FOREIGN KEY(userid) REFERENCES user(id)
);

In the C API, the struct objects are struct C entities, with each field being a structure member. All fields are documented, if provided. Similar to the SQL code, the comments associated with fields and structures are carried into the output.

struct  session {
  struct user user;
  /* User associated with session. */
  int64_t  userid;
  int64_t  id;
};

The query (search), insertion (insert), and deletion (delete) functions are also generated along with their documentation. For example, the search email, hash: name creds snippet generates the following:

/*
 * Lookup by credentials.
 * Uses the given fields in struct user:
 * 	v1: email
 * 	v2: hash (pre-hashed password)
 * Returns a pointer or NULL on fail.
 * Free the pointer with db_user_free().
 */
struct user *db_user_get_creds
  (struct ksql *db, const char *v1, const char *v2);

See the generated C source and header file for yourself: it's all there. One pending caveat is that the output of kwebapp(1) is still OpenBSD-specific, although this may have changed since version 0.3.0.

What can go wrong?

First, exposure to data.

The struct ksql returned by the generated functions allows access to the full database by callers. This is by design: kwebapp doesn't directly support the full breadth of SQL functions you might want to use. By exporting the database connection itself, callers can provide their own complex SQL operations directly to ksql_stmt_alloc(3).

Mistakes happen.

In a recent web application project, I made the mistake of inserting an administrator table row instead of a user table row simply by typo-ing a conditional. If left unchecked, it would have allowed users to create administrators—those with significant control over other users and basically all parts of the system! It would have been a disaster.

How could I have caught this sooner?

Enter RBAC.

With a sandbox like pledge(2), I can stipulate which operations of an application are allowed in which circumstances. For example, if I provide the common -o flag to an application, I might want to provide the wpath cpath pledges; otherwise, I don't allow any file system access. This allows me to say, up-front, what I can do and what I can't.

If I disallow an operation and subsequently try to use it—like running without -o and accessing the file-system—the application crashes. Game over.

As it was previously in kwebapp, there were no restrictions on function invocation—no sandbox. In other words, the landscape was flat. Let's consider an example with a user and admin, representing normal users and administrators, respectively. If the former stipulates an insert operation and the latter, list and insert, we'll have:

However, since the web application would be invoked by an operator in a given role (e.g., a user from his or her web browser, an administrator from a local application or also via the web), I can in theory identify the source and provision accordingly. This way, I can identify which operations were permissable from which user roles.

In the above example, let's say that an administrator should be able to list and add new administrators, but not touch users. Users, on the other hand, can only add other users.

The new kwebapp RBAC feature accomodates for this by identifying roles; and then for each role, establishing which operations are permitted by that role.

If a user role tries to perform a disallowed task—bam. Application calls abort(3). The syntax to accomodate for this is super easy. Using our above example (abridged for brevity)…

roles {
  role user;
  role admin;
};

struct user {
  field email email unique;
  field hash password;
  search email, hash: name creds comment 
    "Lookup by credentials.";
  field id int rowid;
  roles user { 
    search creds;
  };
};

struct session {
  field user struct userid;
  field userid:user.id int comment 
    "User associated with session.";
  field id int rowid;
  insert;
  search id: name id comment 
    "Lookup by unique identifier.";
  delete id;
  roles all {
    all;
  };
  roles default {
    search id;
  };
};

First, note the roles { ... } top-level block. This defines the actual roles themselves. Roles are nested: a role within a role has access to all of the parent's roles, but the parent does not have access to the child's. One common usage is to have administrative sub-roles, for example, role admin { role useradmin; role sessadmin; } might have useradmin manage users, while other roles would be responsible for other tasks, and all admin sub-roles would have access to generic access on the admin table.

There are three reserved roles: default, all, and none. The first is the initial role before a role has been manually set; the second is the top-level for all user-defined roles, and the third cannot do anything at all.

You can read all about the RBAC facilities in kwebapp(5).

The API.

Little needs to change in your system's API. Foremost, however, stipulating a top-level roles { ... }; will change the value returned by db_open(const char *file) from struct ksql to struct kwbp, which is an opaque pointer. This pointer contains the open database connection and our current role.

Next is db_role(struct kwbp *ctx, enum kwbp_role r). This function allows us to change our roles. We can transition from the ROLE_default role into any role, or from any role into a descendent role. We can never change to an ancestor or traverse the role graph. Changing into the same role is a no-op.

You can always drop into ROLE_none, which is guaranteed to be a leaf role. This role can never have operations assigned to it: any database access will fail.

That's it! For most of my applications, the change in returned structure is the most significant part. The rest is fairly mechanical. For the above example, you can see the full C source code and C header file for yourself.

What's next?

kwebapp is designed to be a small system, so there isn't much more to add that wouldn't needlessly complicate things. However…

One likely candidate is to allow the field statement for structures be aware of roles. This is handy when certain roles want to query only some data. For example, consider the following:

struct admin {
  field email email unique;
  field private int comment
    "Some sort of private data.";
  field id int rowid;
  insert;
};

struct user {
  field admin struct adminid;
  field adminid:admin.id int comment 
    "Administrator of this user.";
  field id int rowid;
  insert;
  search id;
};

If we have two roles, admin and user, we can disallow the user from operating on the administrative table. But they can still see the data if we query for the user, as it will be pulled into the exported data. Some of those fields might not be appropriate for users to see.

The problem is usage: there are generally more fields than operations, so it might be a burden to maintain role lists for fields. One solution is to make default-allow, but it still needs consideration.

Another (easier) improvement is to add some syntactic sugar to the role assignments. Right now one can specify all to signify all operations:

struct admin {
  roles all { all; };
};

However, often I want to assign given types of operations to a given user—update, or delete, or searching. It might be helpful to allow, for example, search all to stipulate all search functions.

Acknowledgements

I'd like to thank CAPEM Solutions, Inc., for funding this development and agreeing that it bests serves the community as open source. Also, thanks to Michael Dexter for his copy-editing.