owned this note
owned this note
Published
Linked with GitHub
# dacquiri notes (dan, d0nut, yosh)
github: https://github.com/resyncgg/dacquiri
- let the compiler inform what needs to be done instead of talking to people
- this requires inverting the model from top-to-bottom
- make the compiler enforce that the access-control has occurred for every code path
- this lowers the boundary to author secure software
- annotate the low level methods to do this
## Terminology
- **attributes**: the conditions that you'd typically write if-statements for
- **policies**: the thing we annotate methods with. They're collections of attributes. They need to be enforced ahead of time
- **entities**: the things that can have attributes on them.
- **entity store**: holds onto entities so we can use them later
## Dacquiri example
- say we want to enforce two properties on a function
- user is enabled
- user is owner of document
- we write a function assuming access is already validated
- policy enforces that the properties are checked
- this enforces
```rust
#[get("/documents/{doc_id}")]
async fn access_document(req: HttpRequest, session: Session, doc_id: Path<String>) -> 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
}
```
We want this trait implemented:
```rust
HasEntity<User, "user">
```
```rust
ConstraintChain<..., ..., ConstraintChain<..., .., <etc>>>
```
### How dynamic is this system?
- user is enabled
- can the user be disabled dynamically?
- answer: once you've checked, it's assumed that a property will hold
- particularly optimized for things like short-lived HTTP requests
- You can work around this tho!
- You could have things where you re-check things by revoking permissions.
- May introduce a "live check" attribute
Can we be specific on how this differs from with clauses
- `with`-clauses require you to defer the type ahead of time. Here the attributes are stringly typed.
## with clauses
blog post: https://tmandry.gitlab.io/blog/posts/2021-12-21-context-capabilities/
```rust
capability arena: Arena;
trait Arena {..};
struct BasicArena {..}
```
```rust
use arena::{basic_arena, BasicArena};
struct Bar<'a> {
version: i32,
foo: &'a Foo,
}
// Here the `with` clause is used only for invoking the `Deserialize`
// impl for Foo; we never use `basic_arena` directly.
impl<'a> Deserialize for Bar<'a>
with
basic_arena: &'a BasicArena,
{
fn deserialize(
deserializer: &mut Deserializer
) -> Result<Self, Error> {
let version = deserializer.get_key("version")?;
let foo = deserializer.get_key("foo")?;
Ok(Bar { version, foo })
}
}
```
```rust=
fn access_document()
with user: User
{
...
}
```
```rust=
fn access_document()
with
user: User,
{
...
}
```
```rust=
fn access_domain()
with
user: User,
document: Document,
UserOwnsDocument (with user + document),
{
// stuff
}
```
```rust=
fn toplevel() {
}
fn access_document(doc: AccessibleDocument) {
log(doc.user());
}
```
an admin may want to transfer data from one team to another
- uniqueness of type
- we don't just want to show we can transfer from a team to another team
- we want to be able to talk about _specific instances of types_
Yosh wonders whether we're missing a core type system thing:
```rust
fn square<T: Mult>(n1: T, n2: T) {..}
// no guarantee n1 and n2 are _identical_ values, only identical types
```
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=8933aa6fe11679734c8ff5aa7550f778
```rust
#![feature(generic_const_exprs)]
fn main() {
println!("{}", multi::<10, 10>());
//println!("{}", multi::<10, 12>());
}
fn multi<const N: u32, const M: u32>() -> u32
where
If<{N == M}>: True
{ N * M }
struct If<const Cond: bool> {}
trait True {}
impl True for If<true> {}
```
All of dacquiri is designing around a way to narrow types. In typescript you can have a type which says: "this is an A or a B".
## Alternate
- Instead guaranteeing uniqueness of instance, factor out the authentication and make that return a new type to guarantee the validation.
```rust=
fn square(args: SquareArgs) -> i32 {
args.x() * args.y()
}
fn make_args(x: i32, y: i32) -> SquareArgs {
assert!(x == y);
SquareArgs::new(x, y)
}
```
"from this point onward, guarantee that thing is validated"
```rust
let validated_user = validate_user(&user)?;
some_doc.change_author(validated_user, new_author);
// with with-clauses
with validate_user(&user)? {
some_doc.change_author(new_author);
}
```
- Rather than validating the user and returning a validated user, instead validate the document with the user and return a new type of document.
- From a capability perspective user's are overly coarse-grained
- You want to get rid of the user as soon as you can.
- Instead you want to pass the document around which is a type with the permissions baked in.
```rust
let open_doc: OpenDoc = carefully_open_document(&user, doc_id)?;
open_doc.change_author(new_author);
```
https://blog.yoshuawuyts.com/state-machines-2/
```rust
let open_doc: OpenDoc<DocPermission::Admin> = ...;
let open_doc: Something<OpenDoc> = ...;
```
Thing we haven't covered with this: the intersection stuff (`OR` clauses). We should revisit this. Yosh suspects view types may help here:
```rust
let open_doc: OpenDoc<Permissions { Read: true, Write: true }> = ...;
```
Ergomics rule: if you hit 2/3 states in an enum, methods which are only available on those 2 cases should always be available if you unambiguously know that that's available.