Storing Authenticated Attributes of Known Identities #2621
Replies: 2 comments 3 replies
-
This is great, and it's wonderful to start to see more of this coming together. I have some questions:
I... ended up accidentally doing a deep dive here, since this is very exciting and it began to crystalize in my mind. Note that this more of a suggestion for how it would work than saying it must be how it works. Very important to note that the API sketches below are extremely rough. (Deliberately so -- the point is suggest what a API for storage might take, if we want to keep the ability to make it robust without exposing too many details about the underlying storage directly). Storage Impl
Yes. What the storage is has a few cases:
Storage API
Here's a rough sketch for a high-level storage API for this. This would be the thing implemented on top of the lower-level backend, like SQLite, an abstraction for block-based storage media on embedded, ... The API is fundamentally transactional, and there are two types of transactions: read transactions and write transactions. We'd divide things up along these lines. For concreteness, perhaps something like this. Note that essentially every part of this and the bit at the end are just vague suggestions -- all of this would need further thought and refinement, but it should suggest the API shape I'm imagining for storage. impl Storage {
// ReadTransaction has a methods for performing read-only queries.
// - For example: iteration, fetching by identifier, fetching by property value(s) all seem plausible.
// - prevents concurrent WriteTransactions until the transaction is finished/dropped.
//
// When evaluating is_authorized and such, I think you'd have access to a `&ReadTransaction<'_>`.
// (rather than full `Storage` access and/or full)
async fn begin_read(&self) -> ReadTransaction<'_> { ... }
// WriteTransaction is more powerful:
//
// - While active all concurrent access (both read and write) are blocked.
// - It can perform all the same read queries that ReadTransaction has access to...
// - It can also perform (at a minimum) insert/replace/delete operations.
// (The complete set of mutation operations will need some experimentation to figure out)
//
// - Importantly, `WriteTransaction` must be explicitly committed using a `commit()` function.
// If the `WriteTransaction` is dropped without committing, the transaction is rolled back
// (auto-rollback is a good default in rust because `?` and the like cause early returns
// to be quite common for error cases)
async fn begin_write(&self) -> WriteTransaction<'_> { ... }
}
// see end for slightly more completeness here Sqlite-based storage impls would implement read transactions as Most others are single-process and don't have builtin transactions, so they'd will just implement this on top of a Reader/Writer lock (the write transaction will keep a list of the changes needs to perform on commit, but note that they must appear to be present to the writetransaction prior to the commit) Because the API is transactional, we don't need to provide functions to perform complex queries. Instead, we can just require that they be done in separate steps. For example, it seems reasonable to need to query info about both This keeps the impl fairly simple, while giving significant flexibility to the user in terms of what information they need to access. Slightly more complete api sketchAs above, this is just a sketch. It's provided in case I was too handwavey before. impl ReadTransaction<'_> {
// Fetch by id
async fn lookup(&self, id: &Identifier) -> Result<Option<Identity>>
// iterate over all stored identities.
async fn identities(&self) -> impl Stream<Item = Result<Identity>>
// iterate over identities with matching attributes.
async fn search(&self, required: &Attributes) -> impl Stream<Item = Result<Identity>>
// ...
}
impl WriteTransaction<'_> {
// must be called to apply the changeset.
async fn commit(self) -> Result<()>;
// Some write operations, for example:
// insert new value, error on identifier collison.
async fn insert(&self, identity: Identity) -> Result<()>;
// insert new value or replaces existing. returning old.
async fn replace(&self, identity: Identity) -> Result<Option<Identity>>;
// delete by id
async fn delete(&self, id: &Identifier) -> Result<bool>;
// update identifier attributes (Might be the same as replace actually)
async fn update(&self, id: &Identifier, attrs: &Attributes) -> Result<()>;
// Also all the same ops as ReadTransaction:
async fn lookup(&self, id: &Identifier) -> Result<Option<Identity>>
async fn identities(&self) -> impl Stream<Item = Result<Identity>>
async fn search(&self, required: &Attributes) -> impl Stream<Item = Result<Identity>>
}
type Attributes = BTreeMap<AttributeName, AttributeVaule>;
impl Identity {
fn identifier(&self) -> &Identifier;
fn attributes(&self) -> &Attributes;
} Footnotes
|
Beta Was this translation helpful? Give feedback.
-
I wonder if we should think attribute storage and policy evaluation together? It may also be nice if we would not have to search for subjects (identities) but could rely exclusively on key lookup. If the ABAC implementations act more like oracles that decide based on a few inputs (subject, resource, action), we would not have to transfer attribute sets and policies. There is a short sketch in https://github.com/twittner/ockam/blob/abac/implementations/rust/ockam/ockam_abac/src/ with an implementation for in-memory storage. The trait would be: #[async_trait]
pub trait Abac {
async fn set_subject<I>(&self, s: Subject, attrs: I)
where
I: IntoIterator<Item = (String, Val)> + Send + 'static;
async fn set_policy(&self, r: Resource, a: Action, c: Cond);
async fn del_subject(&self, s: &Subject);
async fn del_policy(&self, r: &Resource);
async fn is_authorised(&self, s: &Subject, r: &Resource, a: &Action) -> bool;
} The in-memory implementation contains a small example: https://github.com/twittner/ockam/blob/dd5441caa2bb764a817d54b66eb76d0840ae5f76/implementations/rust/ockam/ockam_abac/src/mem.rs#L82-L110 |
Beta Was this translation helpful? Give feedback.
-
A few quick definitions:
We want to enable Attribute Based Access Control (ABAC).
In that setting access control / authorization decisions could look something like this:
The body of this function evaluates an authorization policy - it would look at attributes of the
subject
/object
to decided if the requestedaction
is allowed or denied.For example:
The policy enforced by the
is_authorized
function maybesubject.role == "reader"
andsubject.project == "green"
Authenticating that a request is in fact coming from someone with
identifier: 41931e43f51d11dfa9491437f7318d84
AND that the subject withidentifier: 41931e43f51d11dfa9491437f7318d84
has other attributesrole: "reader", project: "green"
can involve complex multistep protocols.In order to decouple authentication of attributes from authorization decisions - I think every Ockam node will need a place to store Authenticated Attributes of Known Identities.
Data would land in this table when various authentication protocols complete.
Authorization steps like the
is_authorized
function above would simply lookup these pre-authenticated attributes given an identifier and attribute name. eg:41931e43f51d11dfa9491437f7318d84, role
or41931e43f51d11dfa9491437f7318d84, project
.There are many inter-related questions here around how does authentication work, where do policies come from etc. Let's tackle those in separate discussion.
Let's focus the conversation below on:
etc.
Beta Was this translation helpful? Give feedback.
All reactions