Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/resyncgg/dacquiri
A strong, compile-time enforced authorization framework for rust applications.
https://github.com/resyncgg/dacquiri
Last synced: 2 months ago
JSON representation
A strong, compile-time enforced authorization framework for rust applications.
- Host: GitHub
- URL: https://github.com/resyncgg/dacquiri
- Owner: resyncgg
- License: mit
- Created: 2022-01-06T13:30:40.000Z (almost 3 years ago)
- Default Branch: main
- Last Pushed: 2023-10-12T11:08:28.000Z (about 1 year ago)
- Last Synced: 2024-10-08T09:38:09.326Z (3 months ago)
- Language: Rust
- Size: 179 KB
- Stars: 343
- Watchers: 5
- Forks: 12
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-rust-security - dacquiri - Attributed based access control (ABAC) framework with compile-time enforcement (Web and Cloud Security / Authorization & Authentication Frameworks)
README
# Dacquiri
A framework that turns authorization vulnerabilities into compiler errors.# What is Dacquiri?
Dacquiri is a framework that uses the type system to validate, at compile time, all code paths satisfy your access control policies. It does this by giving developers the capability to annotate any function with an access control policy. These policies are transformed into complex trait bounds that enforce that callers check that they satisfy these policies ahead of time.# How Does It Work?
Dacquiri consists of two main components: **attributes** and **policies**.## Attributes
*Attributes* are equivalent to the conditions you'd find in your `if` statements and other control flow logic.Take the following example web endpoint in an Actix web application.
```rust
// An example endpoint built in actix.
// Assume that Session is an extractor
#[get("/documents/{doc_id}")]
async fn access_document(req: HttpRequest, session: Session, doc_id: Path) -> impl Responder {
let document_service = req.get_document_service();
let doc_id = doc_id.into_inner();
let document_meta = document_service.fetch_doc_metadata(doc_id).await?;// Only allow caller to read document if they own it
if document_meta.owner == session.user_id {
let document = document_service.fetch_doc_contents(doc_id).await?;Ok(document)
} else {
Err(AppError::Unauthorized)
}
}
```The `document.owner == session.user_id` condition is an example of an *attribute* you'd write in Dacquiri.
Let's build that attribute with Dacquiri and see how we can use it to protect this application.
### Defining an Attribute
```rust
use dacquiri::prelude::*;// define the attribute
#[attribute(Owner)]
fn check_caller_owns_document(session: &Session, document_meta: &DocumentMeta) -> AttributeResult {
// check user owns document
(session.user_id == document_meta.owner)
.then_some(())
.ok_or(AppError::Unauthorized)
}
```Now we can use the `Owner` attribute to talk about whether or not we own a particular document. Now let's use it describe how we could safely fetch documents!
## Policies
*Policies* allow us to define the access control policy on a collection of methods. They're made up of **entities** and **guards**.An *entity* is any object we want to test attributes against or access in our methods.
A *guard* is a collection of attributes that must be satisfied to access this method.
Let's build a simple policy that only allows callers to fetch document contents if they own the document. We'll implement it as an async method on the policy trait definition.
```rust
use dacquiri::prelude::*;#[policy(
entities = (
user: Session,
document_metadata: DocumentMeta
),
guard = (
user is Owner for document_metadata
)
)]
pub trait DocumentOwnerPolicy {
async fn fetch_document_contents(&self, document_service: &DocumentService) -> Result {
// grab the DocumentMeta from our policy definition
let meta: &DocumentMeta = self.get_entity::<_, document_metadata>();// fetch the document contents with the provided document_service
let document = document_service.fetch_doc_contents(meta.doc_id).await?;// return the document!
Ok(document)
}
}
```Policies are defined with a collection of constraints of the form:
```
is [for ]
```Also, notice that we use `self.get_entity` to access the `document_meta` object defined in our policy definition.
Why is this important?
We want to make sure that the `DocumentMeta` object we use to fetch data is the exact same object that we used to validate the access control policy.
This allows us to avoid the following kind of vulnerability:
```rust
let document_meta_one = document_service.fetch_doc_metadata(doc_id_one).await?;
let document_meta_two = document_service.fetch_doc_metadata(doc_id_two).await?;// checking ownership of the first document...
if document_meta_one.owner == session.user_id {
// Ahh! A vulnerability!
// We're fetching the wrong document!
// This uses `document_meta_two` instead of the tested `document_meta_one`
let document = document_service.fetch_doc_contents(document_meta_two.doc_id).await?;Ok(document)
} else {
Err(AppError::Unauthorized)
}
```As long as an entity is defined in our `entities` section of the policy we can fetch it with `get_entity`.
## Using Policies
Now that we have our document fetching method protected by a policy, how do call it?First we need to coalesce the entities we want to prove together into an `EntityProof`. This manages the entities we've added and makes it easy to fetch entities in our policies.
When we add entities to our `EntityProof` we have to give them names. It's not sufficient to just rely on the type of the entity because we may need to talk about two or more entities of the same time. That's why it's important they each have distinct names.
```rust
// coalesce our entities together
let entities = session
.into_entity::<"user">()
.add_entity::<_, "document_metadata">(document_meta)?;
```Next, we check if attributes are true between our entities. We can do this by calling the attribute function, by name, that we defined earlier. For example, we defined the `Owner` attribute function as `check_caller_owns_document(...)` and can call it here.
```rust
// prove `Owner` for "user" and "document_metadata"
let proof = entities.check_caller_owns_document::<"user", "document_metadata">()?;
```Now that we've added the check that proves `"user"` owns the document described by `"document_metadata"`, we can call our protected method!
Let's see this all together.
```rust
#[get("/documents/{doc_id}")]
async fn access_document(req: HttpRequest, session: Session, doc_id: Path) -> impl Responder {
let document_service = req.get_document_service();
let doc_id = doc_id.into_inner();
let document_meta = document_service.fetch_doc_metadata(doc_id).await?;// coalesce our entities
let entities = session
.into_entity::<"user">()
.add_entity::<_, "document_metadata">(document_meta)?;// prove our properties
let proof = entities.check_caller_owns_document::<"user", "document_metadata">()?;// call the protected function!
proof.fetch_document_contents(&document_service).await
}
```Of course you can chain all of these methods together if that makes things easier
# Advanced Attributes
Attributes aren't particularly complex (_partly as a feature_), but they do have some additional capabilities that may not be obvious.## Subject, Resource, and Context
Attribute functions support up to three arguments.The first argument is the **subject** entity. This entity must always be present and be an immutable reference to the entity type.
```rust
#[attribute(Enabled)]
// 'User' is the subject entity type
fn check_user_enabled(user: &User) -> AttributeResult {
// check user is enabled
(user.enabled)
.then_some(())
.ok_or(AppError::Unauthorized)
}
```The second, optional, argument is the **resource** entity. There really isn't a meaningful different between the *subject* and *resource* except where they go in the policy constraint expression. Similar to *subject* entities, *resource* entities must also be an immutable reference to their entity type.
The final possible argument to an attribute function is the **context**. This is any object (_or collection of objects_) that help you verify an attribute. A canonical example of a *context* object is a database connection. Without this connection, you may not be able to query a database and validate some property is true.
A context object _can_ be supplied without an associated resource and may or may not be a reference. If you wish to define an attribute function with only a *subject* entity and a *context* object, set the *resource* entity type to `&()` and it will be ignored.
```rust
#[attribute(Adult)]
fn check_user_is_adult(user: &User, _: &(), db: &DbConnection) -> AttributeResult {
const AGE_ADULT: u32 = 18;
// use db to query user's current age
// we'd *probably* expect this to be a property on `User`, but this is for the sake of the example
let age = db.query_user_age(user.user_id)?;if age >= AGE_ADULT {
Ok(())
} else {
Err(AppError::Unauthorized)
}
}
```## Async Attributes
Attributes can be `async`! There's nothing special you need to do aside from writing the function to be `async`. This is especially useful with *context* objects like database connections or a grpc service.```rust
#[attribute(Member)]
async fn check_user_is_member_of_team(user: &User, team: &Team, service: &TeamService) -> AttributeResult {
// attempt to fetch the membership record of this user
let membership: Option = service.get_membership(user.user_id, team.team_id).await?;if membership.is_some() {
Ok(())
} else {
Err(AppError::UserNotAMember)
}
}
```When you go to test this attribute elsewhere in your code, it'll be an async method that you must `await` on as expected.
## Attribute Name Reuse
Attributes support defining multiple attribute functions allowing for different types of entities to prove a particular attribute. Attributes are still scoped to particular subject and resource entity types, preventing attribute confusion.The main benefit to allowing multiple attribute functions is that different entities can use the same attribute name to describe a relationship. For example, defining the following constraint is non-ideal from a readability perspective.
```rust
#[policy(
entities = (
user: User,
team: Team,
),
guard = (
user is UserEnabled,
team is TeamEnabled,
)
)]
pub trait Something {}
```By defining multiple attribute functions, we can reuse the attribute name `Enabled` but have strong, type-checked attribute proofs for each entity type. To define multiple attribute functions, we annotate a module declaration and place all of our attribute functions inside. We then annotate the attribute functions with `#[attribute]`.
```rust
#[attribute(Enabled)]
mod enabled {
use crate::{User, Team, AppError};
#[attribute]
fn check_user_is_enabled(user: &User) -> AttributeResult {
(user.enabled)
.then_some(())
.ok_or(AppError::UserNotEnabled)
}#[attribute]
fn check_team_is_enabled(team: &Team) -> AttributeResult {
(team.enabled)
.then_some(())
.ok_or(AppError::TeamNotEnabled)
}
}#[policy(
entities = (
user: User,
team: Team,
),
// this reads much better!
guard = (
user is Enabled,
team is Enabled,
)
)]
pub trait Something {}
```# Advanced Policies
## Dependent Policies
In addition to using attributes, guards can depend on other policies by using the following syntax:```
()
```For example, if we created a new policy that relied on our previous `DocumentOwnerPolicy`, we might define the guard in the following way:
```rust
#[policy(
entities = (
user: Session,
document_metadata: DocumentMeta
),
guard = (
user is OtherAttribute,
DocumentOwnerPolicy(user, document_metadata)
)
)]
pub trait OtherPolicy {
// prints document contents to stdout
async fn do_stuff(&self, document_service: &DocumentService) -> Result<(), AppError> {
// we can call `fetch_document_contents` because we're guaranteed to satisfy `DocumentOwnerPolicy`!
let document = self.fetch_document_contents(document_service).await?;println!("Document contents: {}", document);
}
}
```Any methods inside of `OtherPolicy` would be able to call into methods defined by `DocumentOwnerPolicy`. This is true even if we don't explicitly depend on `DocumentOwnerPolicy` in our `guard` statement. As long as all of the policy constraints are known to be satisfied, our method can call methods guarded by other policies.
## Multiple Guards
Sometimes there are multiple contexts in which someone should be able to call into a given method. In our example so far, the caller must prove that a user owns a particular document before retriving its contents. But what if we had a background service that indexed documents for searching? How would that service fetch the document contents without a user session?Policies support multiple guard conditions for such a case. Each guard condition is treated as a branch of an `OR` statement meaning that as long as one of the branches is satisfied, the caller can invoke the policy protected methods.
Let's reinvision our `DocumentOwnerPolicy` to allow for a background service to access the document contents.
```rust
#[policy(
entities = (
user: Session,
service: ServiceSession,
document_metadata: DocumentMeta
),
guard = (
user is Owner for document_metadata
),
guard = (
service is Valid
)
)]
pub trait DocumentOwnerPolicy {
async fn fetch_document_contents(&self, document_service: &DocumentService) -> Result {
// grab the DocumentMeta from our policy definition
let meta: &DocumentMeta = self.get_entity::<_, document_metadata>();// fetch the document contents with the provided document_service
let document = document_service.fetch_doc_contents(meta.doc_id).await?;// return the document!
Ok(document)
}
}
```Unfortunately, there are two major restrictions here.
The first is that if a policy uses multiple guards, no guard may use dependent policies. If it's important that your multi-guard policy be able to call into other policies, you can still require all of the attributes required but cannot depend on the policy itself.
The second restriction is that Dacquiri will require that all described entities are present for the policy to be satisified. That means that despite the fact that we only need a `ServiceSession` to be `Valid` to call into `fetch_document_contents`, we'll need to supply a user's `Session` regardless.
To avoid that problem Dacquiri supports *optional* entities!
## Optional Entities
Optional entities allow us to relax the requirement that described entities are present for a policy to be satisfied. To mark an entity as optional, add a `?` to the end of the type.Taking our previous example, we can mark the `ServiceSession` and `Session` types as optional!
```rust
#[policy(
// 'user' and 'service' are now optional types!
entities = (
user: Session?,
service: ServiceSession?,
document_metadata: DocumentMeta
),
guard = (
user is Owner for document_metadata
),
guard = (
service is Valid
)
)]
pub trait DocumentOwnerPolicy {
async fn fetch_document_contents(&self, document_service: &DocumentService) -> Result {
// grab the DocumentMeta from our policy definition
let meta: &DocumentMeta = self.get_entity::<_, document_metadata>();// fetch the document contents with the provided document_service
let document = document_service.fetch_doc_contents(meta.doc_id).await?;// return the document!
Ok(document)
}
}
```One important thing to note about optional entities is that any entity marked optional will not be able to be retrieved using `self.get_entity`, as this method uses compile-time checks to validate the entity is present.
To access an optional entity, `self.try_get_entity` may be used.