actionable 0.2.0

An enum-based async framework for building permission-driven APIs
Documentation

Actionable provides the basic functionality needed to build an async-based API that has a flexible permissions system integrated.

This crate was designed to be used by BonsaiDb internally, and as a way for users of BonsaiDb to extend their database servers with their own APIs.

Permissions

The [Permissions] struct is constructed from a list of Statements. The Statement struct is inspired by statements in IAM. By default, all actions are denied for all resources.

The [ResourceName] struct describes a unique name/id of anything in your application. This is meant to be similar to ARNs in IAM, but instead of being restricted to a format by this library, you are able to define your own syntax.

The [Action] trait is derive-able, and will convert any enum to something that can be permitted or denied to any [ResourceName]. This derive macro only supports enums with variants that have no parameters, or only have a single name-less parameter that also implements [Action].

An example [Action] enum might look like:

# use actionable::Action;
#[derive(Action, Debug)]
pub enum AllActions {
FlushCache,
User(UserActions),
}

#[derive(Action, Debug)]
pub enum UserActions {
Create,
ChangeUsername,
Delete,
}

An example permissions check for users.42 might look like:

# use actionable::{Action, Permissions, ResourceName};
# #[derive(Action, Debug)]
# pub enum AllActions {
#     FlushCache,
#     User(UserActions)
# }
#
# #[derive(Action, Debug)]
# pub enum UserActions {
#     Create,
#     ChangeUsername,
#     Delete,
# }
# let permissions = Permissions::default();
let allowed = permissions.allowed_to(
&ResourceName::named("users").and(42),
&AllActions::User(UserActions::Delete),
);

Configuration

Along with allowing actions to be taken, Actionable can be used to configure values that may change on a per-role basis. Rate limits are an example of what this API is designed to handle:

# use actionable::{Permissions, Statement, ResourceName, Configuration};
let permissions = Permissions::from(Statement::for_any().with("rate-limit", 500_u64));
let effective_rate_limit = permissions
.get(&ResourceName::named("core-api"), "rate-limit")
.and_then(Configuration::to_unsigned);
assert_eq!(effective_rate_limit, Some(500));

Permission-driven async API

At the core of many networked APIs written in Rust is an enum that represents a request, and similarly there are usually common response/error types. In these applications, there is usually a manually-written match statement that, for readability and maintainability, simply pass the parameters from the request to a helper method to handle the actual logic of the request.

The goal of the API portion of this crate is to replace the aforementioned boilerplate match statement with a simple derive macro. For a commented example, check out actionable/examples/api-simulator.rs.