Dacquiri
An authorization framework with compile-time enforcement.
Introduction to Dacquiri
Dacquiri turns authorization vulnerabilities into compile-time errors.
Dacquiri has two main concepts that govern how authorization policies are defined and applied.
❗ Note - Unstable Features
dacquiri relies on nightly + multiple unstable features to work.
The following unstable features will, at minimum, be required in your
application for it to work with dacquiri.
Additionally, you can add #![allow(incomplete_features)] to ignore the inevitable unstable feature warnings.
Attributes
Attributes are properties we prove about a Subject
(the entity we are applying the authorization check against).
Attributes are statements that are true about a particular subject. For example,
UserIsEnabled may be an attribute defined for User subjects that have their enabled flag set to true.
Some additional attributes you might define could answer the following:
-
Is this user's ID verified?
-
Is this user's account older than 30 days?
-
Is this user a member of a particular team?
The last attribute introduces us to the idea of resources. A Resource is the object a subject is attempting to acquire a particular attribute against. A common example of an attribute, with a resource, would be a
UserIsTeamMemberattribute. For this attribute, the subject isUserand the resource isTeam. This attribute would only be granted if theUserwas a member of the specifiedTeam.While this a useful primitive, it wouldn't make much sense to check if a
Userwas a member of aTeamand then perform actions against a completely differentTeamobject. Therefore, attributes also remember which resource they were acquired against. This way, if necessary, you can access an attribute's associated resource.
Writing Attributes
We define attributes using the attribute macro, 1 to 3 arguments, and an AttributeResult return type.
#
use *;
This will automatically generate an attribute with a User as the subject and () as the resource.
If we have a resource we depend on, we can add it as the second argument to the function.
# use HashSet;
#
#
use *;
The generated UserIsTeamMember attribute will have User as the subject and Team as the resource.
Sometimes, you may not have all of the required information to determine if a subject has a particular attribute
for a particular resource even if you already have that resource fetched. In these cases, you can specify an optional
third argument to provide context or assets required to access additional, required information.
Here's an example iteration on the previous attribute we defined where we fetch data, live, from a database.
# use HashSet;
#
#
# ;
#
use *;
async
You should notice two things that are different about this particular attribute.
- We didn't have to make the context (3rd argument) an immutable reference. Attribute context's can be owned, immutable, or mutable references. This allows you to use any concrete type you wish here.
- You should also notice that this attribute function is
async! Attributes support async and it's as simple as just adding the keyword to the function. All of the other work is handled automatically for you. We'll come back to attributes in a bit, but first let's talk about Entitlements.
Entitlements
Entitlements are traits, gated behind one or more attributes, that are automatically applied
to any subject that has acquired all of the prerequisite attributes at some point, in any order.
An example of a useful entitlement could be a VerifiedUser entitlement which would require the following attributes:
UserIsEnabled- Checks that the user's enabled flag is trueUserIsVerified- Checks that the user's verified state isVerified::Success
Writing Entitlements
Entitlements allow us to guard functionality behind a prerequisite set of attributes using default trait methods.
We start by defining a trait with the entitlement macro.
This entitlement requires that a subject have both the UserIsVerified and UserIsEnabled attributes.
If a subject has acquired both attributes, VerifiedUser will automatically be implemented on the subject.
To get access to the User subject again, we use the get_subject or
get_subject_mut methods. Then we can access information
or make changes to our subject once again.
We can create async methods here as well using #[async_trait] like a normal trait.
Acquiring Attributes
To acquire an attribute, we call one of the following on our subject.
try_granttry_grant_asynctry_grant_with_contexttry_grant_with_context_asynctry_grant_with_resourcetry_grant_with_resource_asynctry_grant_with_resource_and_contexttry_grant_with_resource_and_context_async
For example, if we wanted to check if our User was both enabled and a member of a Team we could do the following.
We'll use the previous UserIsEnabled and UserIsTeamMember attribute definitions.
async
Leveraging Entitlements
Now that we know how to acquire an attribute for a subject, let's put the entitlement system to work by guarding a function with one or more entitlements.
We treat entitlements like regular traits and guard with your favorite trait-bound syntax.
Here's a longer, more complicated example, that demonstrates the value that dacquiri provides
by guarding access to the leave_team functionality to Users until they have checked both
attributes required by the TeamMember entitlement bound.
It does not matter the order that the try_grant_* functions are called, that they are called
sequentially, or that they even happened in the same function.
async
async
Subjects
The last topic that needs to be covered is about subjects. We mentioned them earlier; subjects are the
entities that we're administering an authorization policy against and applying access control.
We do need to denote subjects before we can start acquiring attributes on them.
Do mark a struct as a Subject we mark them with #[derive(Subject))
use Subject;
That's it!
Now you have a relatively good grasp on how dacquiri works and how you can use it
to life authorization requirements into the type system.