Expand description

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.

#![feature(generic_associated_types)]
#![feature(adt_const_params)]
#![feature(generic_arg_infer)]

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 UserIsTeamMember attribute. For this attribute, the subject is User and the resource is Team. This attribute would only be granted if the User was a member of the specified Team.

    While this a useful primitive, it wouldn’t make much sense to check if a User was a member of a Team and then perform actions against a completely different Team object. 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 dacquiri::prelude::*;
#[attribute(UserIsEnabled)]
fn check_user_is_enabled(user: &User) -> AttributeResult<String> {
    match user.enabled {
        true => Ok(()),
        false => Err(format!("User is not enabled."))
    }
}

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 dacquiri::prelude::*;
#[attribute(UserIsTeamMember)]
fn check_user_team(
    user: &User,
    team: &Team
) -> AttributeResult<String> {
    match team.users.contains(&user.user_id) {
        true => Ok(()),
        false => Err(format!("User is not specified team."))
    }
}

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 dacquiri::prelude::*;
#[attribute(UserIsTeamMember)]
async fn user_team_check(
    user: &User,
    team: &Team,
    conn: &mut DatabaseConnection
) -> AttributeResult<String> {
    let row_count = conn.count_query(
        "select count(*) from memberships where uid = {} and tid = {}",
        vec![user.user_id, team.team_id]
    )
    .await
    .map_err(|_| format!("DB error."))?;
    // if we have more than 1 records, we're on the team!
    match row_count > 0  {
        true => Ok(()),
        false => Err(format!("User is not on the specified team."))
    }
}

You should notice two things that are different about this particular attribute.

  1. 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.
  2. 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 true
  • UserIsVerified - Checks that the user’s verified state is Verified::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.

#[entitlement(UserIsVerified, UserIsEnabled)]
pub trait VerifiedUser {
    fn print_message(&self) {
        println!("Hello, world!!");
    }
}

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.

#[entitlement(UserIsVerified, UserIsEnabled)]
pub trait VerifiedUser {
    fn change_name(&mut self, new_name: impl Into<String>) {
        self.get_subject_mut().name = new_name.into();
    }
}

We can create async methods here as well using #[async_trait] like a normal trait.

#[async_trait]
#[entitlement(UserIsVerified, UserIsEnabled)]
pub trait VerifiedUser {
    // set the account's enabled to false and consume the user
    async fn disable_account(self, conn: &mut DatabaseConnection) {
        let query = escape!(
            "UPDATE users SET enabled = false WHERE uid = {};",
            self.get_subject().user_id
        );
        conn.execute(query).await;
    }
}

Acquiring Attributes

To acquire an attribute, we call one of the following on our subject.

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.

#[tokio::main]
async fn main() -> Result<(), String> {
    let user: User = get_user();
    let team: Team = get_team();
    let mut conn: DatabaseConnection = get_database_conn();
    let checked_user = user
        .try_grant::<UserIsEnabled>()?
        .try_grant_with_resource_and_context_async::<UserIsTeamMember, _>(team, &mut conn).await?;
}

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.

#[tokio::main]
async fn main() -> Result<(), String> {
    let user: User = get_user();
    let team: Team = get_team();
    let mut conn: DatabaseConnection = get_database_conn();
    let mut checked_user = user
        .try_grant::<UserIsEnabled>()?
        .try_grant_with_resource_and_context_async::<UserIsTeamMember, _>(team, &mut conn).await?;
    leave_my_account(&mut checked_user).await
}
async fn leave_my_team(user: impl TeamMember) -> Result<(), String> {
    // you can't call `.leave_team()` if you're not
    // a TeamMember (which requires UserIsEnabled and UserIsTeamMember)
    user.leave_team().await
}
#[entitlement(UserIsEnabled, UserIsTeamMember)]
#[async_trait]
trait TeamMember {
    // we capture self here because leaving the team
    // means we're no longer a team member
    async fn leave_team(
        self,
        conn: &mut DatabaseConection
) -> Result<(), String> {
        let user = self.get_subject();
        // we need to specify *which* attribute's resource we want
        let team = self.get_resource::<UserIsTeamMember, _, _>();
        let query = escape!(
            "DELETE FROM members WHERE uid = {} AND tid = {};",
            user.user_id,
            team.team_id
        );
        conn
            .execute(query)
            .await
            .map(|_| format!("DB error"))?;
        Ok(())
    }
}

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 dacquiri::prelude::Subject;
#[derive(Subject)]
pub struct AuthenticatedUser {
    username: String,
    session_token: String,
    enabled: bool
}

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.

Modules