Derive Macro bonsaidb::core::actionable::Actionable
#[derive(Actionable)]
{
// Attributes available to this derive:
#[actionable]
}
Expand description
Derives a set of traits that can be used to implement a permissions-driven
API. There are options that can be customized with the #[actionable]
attribute at the enum level:
- Crate name override:
#[actionable(actionable = "someothername")]
. If you find yourself needing to importactionable
as another name, this setting will replace all mentions ofactionable
with the identifier specified.
The Dispatcher Trait
The first trait that is generated is named <EnumName>Dispatcher
. For
example, if the enum’s name is Request
, the generated trait name will be
RequestDispatcher
. This trait has no methods for you to implement. It
defines several associated types:
Output
: TheOk
side of theResult
.Error
: TheErr
side of theResult
. Must implementFrom<actionable::PermissionDenied>
.- For each variant in the enum, another Trait named
<VariantName>Handler
. For example, if the enum variant wasRequest::AddUser
, the trait will beAddUserHandler
. Each of these traits must be implemented by any type implementing<EnumName>Dispatcher
.
The dispatcher trait has a method available for you to use to dispatch
requests: async fn dispatch(&self, permissions: &Permissions, request: <EnumName>) -> Result<Self::Output, Self::Error>
.
The Handler Traits
For each variant in the enum, a trait will be generated named
<VariantName>Handler
. Using the same example above, the enum variant
Request::AddUser
would generate the trait AddUserHandler
. These traits
are implemented using the
async-trait
trait.
Each variant must have a protection method assigned using the
#[actionable]
attribute. There are three protection methods:
No Protection: #[actionable(protection = "none")]
A handler with no protection has one method:
#[async_trait]
trait Handler {
type Dispatcher;
async fn handle(
dispatcher: &Self::Dispatcher,
permissions: &Permissions,
/* each field on this variant is passed
as a parameter to this method */
) -> Result<Output, Error>;
}
Actionable does not do any checks before invoking this handler.
Simple Protection: #[actionable(protection = "simple")]
A handler with simple protection exposes methods and types to allow
specifying an actionable::ResourceName
and an Action
for this handler:
#[async_trait]
trait Handler {
type Dispatcher;
type Action;
fn resource_name<'a>(
dispatcher: &Self::Dispatcher,
/* each field on this variant is passed
by reference as a parameter to this method */
) -> ResourceName<'a>;
fn action() -> Self::Action;
async fn handle_protected(
dispatcher: &Self::Dispatcher,
permissions: &Permissions,
/* each field on this variant is passed
as a parameter to this method */
) -> Result<Output, Error>;
}
When the handler is invoked, it first checks permissions
to ensure that
action()
is allowed to be performed on resource_name()
. If it is not
allowed, an actionable::PermissionDenied
error will be returned. If it is
allowed, handle_protected()
will be executed.
Custom Protection: #[actionable(protection = "custom")]
A handler with custom protection has two methods, one to verify permissions and one to execute the protected code:
#[async_trait]
trait Handler {
type Dispatcher;
async fn verify_permissions(
dispatcher: &Self::Dispatcher,
permissions: &Permissions,
/* each field on this variant is passed
by refrence as a parameter to this method */
) -> Result<(), Error>;
async fn handle_protected(
dispatcher: &Self::Dispatcher,
permissions: &Permissions,
/* each field on this variant is passed as a parameter
to this method */
) -> Result<Output, Error>;
}
Actionable will first call verify_permissions()
. If you return Ok(())
,
your handle_protected()
method is invoked.
Why should you use the built-in protection modes?
Actionable attempts to make permission handling easy to understand and implement while making it difficult to forget implementing permission handling. This is only effective if you use the protection levels.
Because Actionable includes permissions
in every call to
handle[_protected]()
, technically you could use a protection level of
none
and implement permission handling within the handle()
function.
While it would work, you shouldn’t do this.
Actionable encourages placing information about permission handling in the
definition of the enum. By using simple
and custom
protection
strategies, consumers of your API will be able to see at the enum level what
APIs check permissions. When trying to understand what permissions are being
used, this is critical.
By placing your permission handling code in locations that follow a repeatable patern, you’re helping anyone who is reading the code separate what logic is related to permission handling and what logic is related to the API implementation.
What protection mode should you use?
- If your handler is operating on a single resource and performing a single
action, use the
simple
protection mode. - If your handler needs to check permissions but it’s more complicated than
the first scenario, use the
custom
protection mode. - If you aren’t enforcing permissions inside of this handler, use the
none
protection mode.