How do we handle authorization?
This is an in progress document containing the brainstorming around how we want to handle authorization in bindle. This comes from a HackMD where various maintainers collaborated on figuring things out and it has been cleaned up and updated. Any ideas or feedback is welcome!
Goals/Non-goals
The following goals and non-goals are specifically for 1.0. This doesn't mean we don't want them in the future.
Goals:
- OAuth tokens can be authorized against resources
- File-based ACL rules
Non-goals:
- Database (or other datasource) ACL rule sources
- Role configuration endpoint (TODO: Maybe we actually want this for 1.0?)
- Per parcel rules (i.e. access to only specific parcels of a Bindle)
Use cases
Verbs
There are two main verbs used across all bindle resources:
getcreate
For invoices, there is an additional verb called yank. If necessary, this could be changed to delete to match other conventions.
Expected user interactions
Below is the list of expected interactions with the Bindle server covered by authorization flow:
TODO: Allow version specific restrictions
- Anonymous read access: This will be the default authorization mode for all bindles (a la dockerhub)
- Read (
get) access: A user can only access an invoice and its parcels if logged in and granted access- This kind of access can be granted on specific bindles (example.com/foo), but not specific versions of a bindle (example.com/foo/1.0.0)
- This access can also be granted on any subpath of a bindle. So given a bindle of
example.com/catblog/foo/1.0.0, access can be granted to anything under theexample.comsubpath or it can be granted toexample.com/catblogto further restrict access
- Create (
create) access: A user can only create an invoice and upload parcels for invoices if logged in and granted access- This kind of access can be granted on specific bindles (example.com/foo), but not specific versions of a bindle (example.com/foo/1.0.0)
- This access can also be granted on any subpath of a bindle. So given a bindle of
example.com/catblog/foo/1.0.0, access can be granted to anything under theexample.comsubpath or it can be granted toexample.com/catblogto further restrict access. In theexample.comcase, a user would be able to create invoices and upload parcels with any ID starting withexample.com. In theexample.com/catblogexample, a user would be able to create any number of bindles and parcels under that subpath.
- Yank (
yank) access: A user can only yank an invoice if logged in and granted access- This kind of access can be granted on specific invoices (example.com/foo), but not specific versions of an invoice (example.com/foo/1.0.0)
- This access can also be granted on any subpath of a bindle. So given a bindle of
example.com/catblog/foo/1.0.0, access can be granted to anything under theexample.comsubpath or it can be granted toexample.com/catblogto further restrict access. In theexample.comcase, a user would be able to yank invoices with any ID starting withexample.com. In theexample.com/catblogexample, a user would be able to yank any number of invoices under that subpath.
Type restrictions
It is important to note that any access control lists will also need to specify types due to the arbitrarily pathy nature of Bindle IDs. When mapping roles for an invoice, an additional type restriction must be specified. These types are specified below:
bindle: Indicates that this path is a full bindle path and isn't a subpath. So acreaterole onexample.com/foowith abindletype means that only anexample.com/foobindle can be created. It will prevent a user from creating anexample.com/foo/barbindle. This is the default rule if no type is specifiedsubpath: Indicates that this path is a subpath, granting the user the right to do anything with the allowed verb under that subpath. So acreaterole onexample.com/foowith asubpathtype means that user can arbitrarily create any bindle starting with theexample.com/foosubpath, but CANNOT create a bindle with that exact path.
Authorization flow
This section discusses how the authorization flow will work in our code at a high level.
Groups/User mapping
Both types of authentication must have a type that implements the Authorizable trait:
pub trait Authorizable {
/// Returns the identity or username of the authenticated user
fn principal(&self) -> String;
/// Returns the groups the authenticated user is a member of, generally embedded on something
/// like a JWT or fetched from an upstream server
fn groups(&self) -> Vec<String>;
}
This will allow the server to be able to check the authenticated user against the configured ACL list.
Token based
The username from a JWT will be pulled from the sub claim. If this is not present, the token will not be accepted. For simpler group management (i.e. not needing to contact an external server to fetch the groups), we will require the presence of a custom groups claim. This is a common field on many JWTs and so should not be much of a hassle to use with third party OAuth providers. The list of groups from the token would be returned wholesale using the groups() method.
Option 2: Biscuit tokens
Another option could be to use biscuit tokens, as a way to contain the ACL for the request rather than with specific file based ACL rules (as described below)
ACL creation and maintenance
For 1.0, the plan will be to only use a config file for ACL policies (most likely a TOML file). The structure of this file will be determined by the library we end up using for the rules engine and so it is not defined here.
As stated before, database support is non-goal, but is something we want to add in the future. Accessing the rules should be behind a trait for easy extension in the future.
Rules engine
This is the biggest unknown at the current time. Early ideas for the rules engine involve something similar to the Upspin project for handling the ACLs and actual authorization. However, this project is written in Go and would be hard to consume within Rust. It does lead us to the other option of implementing our own rules engine, possibly based on Upspin. The downside of this option, no matter how it is implemented, is that we'd have a whole engine to use from scratch. This upside is we could design it as a separate crate with flexibility in rule sources and contribute something useful to the Rust community. The Oso project also looks interesting, but it is new and little-used. It also has the additional downside of locking us into its custom "Polar" language format for our rules.