diff options
Diffstat (limited to 'crates/secd/src/auth')
| -rw-r--r-- | crates/secd/src/auth/n.rs | 203 | ||||
| -rw-r--r-- | crates/secd/src/auth/z/graph.rs | 416 | ||||
| -rw-r--r-- | crates/secd/src/auth/z/mod.rs | 4 |
3 files changed, 361 insertions, 262 deletions
diff --git a/crates/secd/src/auth/n.rs b/crates/secd/src/auth/n.rs index 1f32fd6..dde6e7d 100644 --- a/crates/secd/src/auth/n.rs +++ b/crates/secd/src/auth/n.rs @@ -5,12 +5,12 @@ use crate::{ DEFAULT_SIGNUP_EMAIL, }, store::{ - AddressLens, AddressValidationLens, CredentialLens, IdentityLens, SessionLens, - Storable, StoreError, + AddressLens, AddressValidationLens, CredentialLens, IdentityLens, Storable, StoreError, }, }, - util, Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod, - Credential, CredentialType, Identity, IdentityId, Secd, SecdError, Session, SessionToken, + util::{self, ErrorContext}, + Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod, + Credential, CredentialId, CredentialType, Identity, IdentityId, Secd, SecdError, ADDRESSS_VALIDATION_CODE_SIZE, ADDRESS_VALIDATION_ALLOWS_ATTEMPTS, ADDRESS_VALIDATION_IDENTITY_SURJECTION, EMAIL_VALIDATION_DURATION, }; @@ -84,8 +84,8 @@ impl Secd { revoked_at: None, validated_at: None, attempts: 0, - hashed_token: util::hash(&secret.as_bytes()), - hashed_code: util::hash(&code.as_bytes()), + hashed_token: util::hash(secret.as_bytes()), + hashed_code: util::hash(code.as_bytes()), }; validation.write(self.store.clone()).await?; @@ -95,11 +95,13 @@ impl Secd { .cfg .email_address_from .clone() + .and_then(|s| s.parse().ok()) .unwrap_or("SecD <noreply@secd.com>".parse().unwrap()), replyto_address: self .cfg .email_address_replyto .clone() + .and_then(|s| s.parse().ok()) .unwrap_or("SecD <noreply@secd.com>".parse().unwrap()), recipient: email_address.clone(), subject: "Login Request".into(), @@ -129,7 +131,7 @@ impl Secd { validation_id: &AddressValidationId, plaintext_token: Option<String>, plaintext_code: Option<String>, - ) -> Result<Session, SecdError> { + ) -> Result<Credential, SecdError> { let mut validation = AddressValidation::find( self.store.clone(), &AddressValidationLens { @@ -193,7 +195,6 @@ impl Secd { id: None, address_type: Some(&validation.address.t), validated_address: Some(true), - session_token_hash: None, }, ) .await?; @@ -210,6 +211,7 @@ impl Secd { id: Uuid::new_v4(), address_validations: vec![], credentials: vec![], + new_credentials: vec![], rules: vec![], metadata: None, created_at: OffsetDateTime::now_utc(), @@ -235,30 +237,72 @@ impl Secd { validation.validated_at = Some(OffsetDateTime::now_utc()); validation.write(self.store.clone()).await?; - let session = Session::new(validation.identity_id.expect("unreachable d3ded289-72eb-4a42-a37d-f5c9c697cc61 [assert(identity.is_some()) prevents this]"))?; + let mut session = Credential::new_session(validation.identity_id.expect("unreachable d3ded289-72eb-4a42-a37d-f5c9c697cc61 [assert(identity.is_some()) prevents this]"))?; + let plaintext_type = session.t.clone(); + + session.hash(&self.crypter)?; session.write(self.store.clone()).await?; + session.t = plaintext_type; + Ok(session) } + pub async fn create_identity_with_credential( + &self, + t: CredentialType, + identity_id: IdentityId, + metadata: Option<String>, + ) -> Result<Identity, SecdError> { + let identity = Identity::find( + self.store.clone(), + &IdentityLens { + id: Some(&identity_id), + address_type: None, + validated_address: None, + }, + ) + .await?; + + if !identity.is_empty() { + log::error!("identity was found while creating a new identity with a credential"); + return Err(SecdError::IdentityAlreadyExists); + } + + Identity { + id: identity_id, + address_validations: vec![], + credentials: vec![], + new_credentials: vec![], + rules: vec![], + metadata, + created_at: OffsetDateTime::now_utc(), + deleted_at: None, + } + .write(self.store.clone()) + .await?; + + self.create_credential(t, Some(identity_id), None).await + } + pub async fn create_credential( &self, t: CredentialType, identity_id: Option<IdentityId>, + expires_at: Option<OffsetDateTime>, ) -> Result<Identity, SecdError> { - let identity = match identity_id { + let mut identity = match identity_id { Some(id) => Identity::find( self.store.clone(), &IdentityLens { id: Some(&id), address_type: None, validated_address: None, - session_token_hash: None, }, ) .await? .into_iter() - .nth(0) + .next() .ok_or(SecdError::IdentityNotFound)?, None => { @@ -266,6 +310,7 @@ impl Secd { id: Uuid::new_v4(), address_validations: vec![], credentials: vec![], + new_credentials: vec![], rules: vec![], metadata: None, created_at: OffsetDateTime::now_utc(), @@ -282,7 +327,6 @@ impl Secd { id: None, identity_id: Some(identity.id), t: Some(&t), - restrict_by_key: Some(false), }, ) .await?[..] @@ -290,9 +334,9 @@ impl Secd { [] => Credential { id: Uuid::new_v4(), identity_id: identity.id, - t, + t: t.clone(), created_at: OffsetDateTime::now_utc(), - revoked_at: None, + revoked_at: expires_at, deleted_at: None, }, _ => return Err(SecdError::CredentialAlreadyExists), @@ -307,57 +351,100 @@ impl Secd { err => SecdError::StoreError(err), })?; - Ok(identity) - } + identity.new_credentials.push(Credential { + id: credential.id, + identity_id: credential.identity_id, + t, + created_at: credential.created_at, + revoked_at: credential.revoked_at, + deleted_at: credential.deleted_at, + }); - pub async fn validate_credential( - &self, - // t: CredentialType, - // key: String, - // value: Option<String>, - ) -> Result<Session, SecdError> { - // Credential::find(store, lens) use key here as unique index - todo!() + Ok(identity) } - pub async fn get_session(&self, t: &SessionToken) -> Result<Session, SecdError> { - let token = hex::decode(t)?; - let mut session = Session::find( + pub async fn validate_credential(&self, t: CredentialType) -> Result<Credential, SecdError> { + let mut retrieved = Credential::find( self.store.clone(), - &SessionLens { - token_hash: Some(&util::hash(&token)), + &CredentialLens { + id: None, identity_id: None, + t: Some(&t), }, ) - .await?; - assert!(session.len() <= 1, "get session failed: multiple sessions found for a single token. This is very _very_ bad."); + .await? + .into_iter() + .next() + .ok_or(SecdError::InvalidCredential)?; - if session.is_empty() { - return Err(SecdError::InvalidSession); - } else { - let mut session = session.swap_remove(0); - session.token = token; - Ok(session) - } + match retrieved.revoked_at { + Some(t) if t <= OffsetDateTime::now_utc() => { + log::debug!("credential was revoked"); + Err(SecdError::InvalidCredential) + } + _ => Ok(()), + }?; + + match retrieved.deleted_at { + Some(t) if t <= OffsetDateTime::now_utc() => { + log::debug!("credential was deleted"); + Err(SecdError::InvalidCredential) + } + _ => Ok(()), + }?; + + retrieved.hash_compare(&t, &self.crypter)?; + + // Return the initially provided plaintext credential since it's valid + retrieved.t = t; + + Ok(retrieved) } pub async fn get_identity( &self, i: Option<IdentityId>, - t: Option<SessionToken>, + t: Option<CredentialType>, ) -> Result<Identity, SecdError> { - let token_hash = match t { - Some(tok) => Some(util::hash(&hex::decode(&tok)?)), - None => None, - }; + if i.is_none() && t.is_none() { + log::error!("get_identity expects that at least one of IdentityId or CredentialType is provided. None were found."); + return Err(SecdError::IdentityNotFound); + } + + let c = Credential::find( + self.store.clone(), + &CredentialLens { + id: None, + identity_id: i, + t: t.as_ref(), + }, + ) + .await?; + + assert!( + c.len() <= 1, + "The provided credential refers to more than one identity. This is very _very_ bad." + ); + let identity_id = c + .into_iter() + .next() + .ok_or(SecdError::InvalidCredential) + .ctx("No identities were found for the provided identity_id and credential_type")? + .identity_id; + + if i.is_some() && i != Some(identity_id) { + log::error!( + "The provided identity does not match the identity associated with this credential" + ); + return Err(SecdError::InvalidCredential); + } let mut i = Identity::find( self.store.clone(), &IdentityLens { - id: i.as_ref(), + id: Some(&identity_id), address_type: None, validated_address: None, - session_token_hash: token_hash, }, ) .await?; @@ -368,7 +455,7 @@ impl Secd { ); if i.is_empty() { - return Err(SecdError::IdentityNotFound); + Err(SecdError::IdentityNotFound) } else { Ok(i.swap_remove(0)) } @@ -385,12 +472,11 @@ impl Secd { id: Some(&i), address_type: None, validated_address: None, - session_token_hash: None, }, ) .await? .into_iter() - .nth(0) + .next() .ok_or(SecdError::IdentityNotFound)?; identity.metadata = Some(md); @@ -399,9 +485,22 @@ impl Secd { Ok(identity) } - pub async fn revoke_session(&self, session: &mut Session) -> Result<(), SecdError> { - session.revoked_at = Some(OffsetDateTime::now_utc()); - session.write(self.store.clone()).await?; + pub async fn revoke_credential(&self, credential_id: CredentialId) -> Result<(), SecdError> { + let mut credential = Credential::find( + self.store.clone(), + &CredentialLens { + id: Some(credential_id), + identity_id: None, + t: None, + }, + ) + .await? + .into_iter() + .next() + .ok_or(SecdError::InvalidCredential)?; + + credential.revoked_at = Some(OffsetDateTime::now_utc()); + credential.write(self.store.clone()).await?; Ok(()) } } diff --git a/crates/secd/src/auth/z/graph.rs b/crates/secd/src/auth/z/graph.rs index 9ca045b..f28c901 100644 --- a/crates/secd/src/auth/z/graph.rs +++ b/crates/secd/src/auth/z/graph.rs @@ -1,209 +1,207 @@ -use std::collections::{HashSet, VecDeque}; - -type NodeIdx = usize; -type EdgeIdx = usize; - -struct RelationGraph { - nodes: Vec<Node>, - edges: Vec<Edge>, -} - -#[derive(Hash, PartialEq, Eq)] -struct Node { - id: u64, - is_namespace: bool, - edge_list_head: Option<EdgeIdx>, -} - -struct Edge { - dst: NodeIdx, - op: Option<Operator>, - next_edge: Option<EdgeIdx>, -} - -enum Operator { - And, - Or, - Difference, - Complement, - AndAll, - OrAll, - DifferenceAll, - ComplementAll, -} - -impl RelationGraph { - pub fn new() -> Self { - // As I think about how to serialize this graph, when reading from the DB - // I should chunk up the namespaces so that I can read-on-demand when I - // encounter a namespace that has not yet been loaded, and in this way - // I don't need to deal with the entire graph... - // - // but that's a totally unnecessary fun optimization. - RelationGraph { - nodes: vec![], - edges: vec![], - } - } - - pub fn add_node(&mut self, id: u64, is_namespace: bool) -> NodeIdx { - let idx = self.nodes.len(); - self.nodes.push(Node { - id, - is_namespace, - edge_list_head: None, - }); - idx - } - - pub fn add_edge( - &mut self, - src: NodeIdx, - dst: NodeIdx, - op: Option<Operator>, - follow: Option<NodeIdx>, - ) { - let edge_idx = self.edges.len(); - let node = &mut self.nodes[src]; - // TODO: dupe check - self.edges.push(Edge { - dst, - op, - next_edge: node.edge_list_head, - }); - node.edge_list_head = Some(edge_idx); - } - - // doc:2#viewer@user:1 - pub fn find_leaves(&self, src: NodeIdx, filterIdx: Option<NodeIdx>) -> Vec<NodeIdx> { - let mut all_leaves = vec![]; - // let mut and_leaves = vec![]; - // let mut exc_leaves = vec![]; - - // the output should basically be leaves to look up - // for example - // the leaves of doc might be (user), <(organization, admin)>, <(role, foo)> - // and since (organization, admin) has leaves (user) as well - // if the user is in that final set, then they are valid - // and those leaves would be queried as relation tuples saved in the DB - - println!("HERE I AM"); - - let start = self.nodes.get(src); - let mut seen = HashSet::new(); - let mut q = VecDeque::new(); - - seen.insert(start); - q.push_back(start); - - // I need to build a chain of ops - // [AND, OR, OR, AND, OR, ...] - // at each stage, I need to apply the operator chain - while let Some(&Some(node)) = q.iter().next() { - if node.is_namespace { - all_leaves.push(node); - } - - // for edge in edge list - - println!("IN QUEUE\n"); - println!("node: {:?}\n", node.id); - q.pop_front(); - } - println!("DONE"); - - // task...starting at doc_viewer, return all leaf nodes - // to do this.... - // check all children - // if child is OR - // if child is leaf, return child - // else return all of these children - // if child is AND - // if child is leaf, return if also present in all other nodes - // else return all children which are also present in other nodes - // if child is Difference - // if child is leaf, do not include and remove if it part of the user set - // else remove all of these children - - // TOTAL pool of leafs - // Intersection leaves (i.e. ONLY leaves which MUST exist) - // exclusion leaves (i.e. leaves which CANNOT exist) - - // bfs build query... - // execute query - - // start at nodeIdx: doc_viewer - // expand until I find all leaf nodes. - // we are interested in leaf nodes with nodeIdx: user - // return all leaf nodeIdx that match the filter. If no filter provided, return all leaf idx. - - // TODO: optionally build the expansion path by pushing every intermediate step as the bfs is walked. - - // with each step of the bfs, perform the operator. - // e.g. if first step is doc_viewer -> user with operator OR then we have leaf user with operator chain [OR] - // the next step might be doc_viewer -> doc_editor with step OR and doc_editor -> user with step OR, so we have leaf user with chain [OR, OR] - // the next step might be doc_viewer -> doc_auditor with OR - // then doc_auditor -> doc_editor with OR - // then doc_editor -> doc_user with OR - // then doc_editor -> doc_owner - // then doc_auditor -> doc_owner with Difference - // - - todo!() - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn build_graph_test() { - let mut g = RelationGraph::new(); - let user = g.add_node(1, true); - let role = g.add_node(2, true); - let group = g.add_node(3, true); - let doc = g.add_node(4, true); - - let role_member = g.add_node(5, false); - - let group_member = g.add_node(6, false); - let group_admin = g.add_node(7, false); - - let doc_owner = g.add_node(8, false); - let doc_editor = g.add_node(9, false); - let doc_viewer = g.add_node(10, false); - let doc_auditor = g.add_node(11, false); - let doc_parent = g.add_node(12, false); - - g.add_edge(role, role_member, None, None); - g.add_edge(role_member, user, Some(Operator::Or), None); - g.add_edge(role_member, group_member, Some(Operator::Or), None); - - g.add_edge(group, group_member, None, None); - g.add_edge(group, group_admin, None, None); - - g.add_edge(group_member, user, Some(Operator::Or), None); - g.add_edge(group_member, group_admin, Some(Operator::Or), None); - g.add_edge(group_admin, user, Some(Operator::Or), None); - - g.add_edge(doc, doc_owner, None, None); - g.add_edge(doc, doc_editor, None, None); - g.add_edge(doc, doc_viewer, None, None); - g.add_edge(doc, doc_auditor, None, None); - g.add_edge(doc, doc_parent, None, None); - - g.add_edge(doc_owner, user, Some(Operator::Or), None); - g.add_edge(doc_editor, user, Some(Operator::Or), None); - g.add_edge(doc_editor, doc_owner, Some(Operator::Or), None); - g.add_edge(doc_viewer, user, Some(Operator::Or), None); - g.add_edge(doc_viewer, doc_editor, Some(Operator::Or), None); - g.add_edge(doc_viewer, doc_parent, Some(Operator::Or), Some(doc_viewer)); - g.add_edge(doc_viewer, user, Some(Operator::OrAll), None); - g.add_edge(doc_viewer, doc_auditor, Some(Operator::Or), None); - g.add_edge(doc_auditor, doc_editor, Some(Operator::Difference), None); - - g.find_leaves(doc_viewer, None); - assert_eq!(1, 2); - } -} +// use std::collections::{HashSet, VecDeque}; + +// type NodeIdx = usize; +// type EdgeIdx = usize; + +// struct RelationGraph { +// nodes: Vec<Node>, +// edges: Vec<Edge>, +// } + +// #[derive(Hash, PartialEq, Eq)] +// struct Node { +// id: u64, +// is_namespace: bool, +// edge_list_head: Option<EdgeIdx>, +// } + +// struct Edge { +// dst: NodeIdx, +// op: Option<Operator>, +// next_edge: Option<EdgeIdx>, +// } + +// enum Operator { +// And, +// Or, +// Difference, +// Complement, +// AndAll, +// OrAll, +// DifferenceAll, +// ComplementAll, +// } + +// impl RelationGraph { +// pub fn new() -> Self { +// // As I think about how to serialize this graph, when reading from the DB +// // I should chunk up the namespaces so that I can read-on-demand when I +// // encounter a namespace that has not yet been loaded, and in this way +// // I don't need to deal with the entire graph... +// // +// // but that's a totally unnecessary fun optimization. +// RelationGraph { +// nodes: vec![], +// edges: vec![], +// } +// } + +// pub fn add_node(&mut self, id: u64, is_namespace: bool) -> NodeIdx { +// let idx = self.nodes.len(); +// self.nodes.push(Node { +// id, +// is_namespace, +// edge_list_head: None, +// }); +// idx +// } + +// pub fn add_edge( +// &mut self, +// src: NodeIdx, +// dst: NodeIdx, +// op: Option<Operator>, +// follow: Option<NodeIdx>, +// ) { +// let edge_idx = self.edges.len(); +// let node = &mut self.nodes[src]; +// // TODO: dupe check +// self.edges.push(Edge { +// dst, +// op, +// next_edge: node.edge_list_head, +// }); +// node.edge_list_head = Some(edge_idx); +// } + +// // doc:2#viewer@user:1 +// pub fn find_leaves(&self, src: NodeIdx, filterIdx: Option<NodeIdx>) -> Vec<NodeIdx> { +// let mut all_leaves = vec![]; +// // let mut and_leaves = vec![]; +// // let mut exc_leaves = vec![]; + +// // the output should basically be leaves to look up +// // for example +// // the leaves of doc might be (user), <(organization, admin)>, <(role, foo)> +// // and since (organization, admin) has leaves (user) as well +// // if the user is in that final set, then they are valid +// // and those leaves would be queried as relation tuples saved in the DB + +// let start = self.nodes.get(src); +// let mut seen = HashSet::new(); +// let mut q = VecDeque::new(); + +// seen.insert(start); +// q.push_back(start); + +// // I need to build a chain of ops +// // [AND, OR, OR, AND, OR, ...] +// // at each stage, I need to apply the operator chain +// while let Some(&Some(node)) = q.iter().next() { +// if node.is_namespace { +// all_leaves.push(node); +// } + +// // for edge in edge list + +// println!("IN QUEUE\n"); +// println!("node: {:?}\n", node.id); +// q.pop_front(); +// } +// println!("DONE"); + +// // task...starting at doc_viewer, return all leaf nodes +// // to do this.... +// // check all children +// // if child is OR +// // if child is leaf, return child +// // else return all of these children +// // if child is AND +// // if child is leaf, return if also present in all other nodes +// // else return all children which are also present in other nodes +// // if child is Difference +// // if child is leaf, do not include and remove if it part of the user set +// // else remove all of these children + +// // TOTAL pool of leafs +// // Intersection leaves (i.e. ONLY leaves which MUST exist) +// // exclusion leaves (i.e. leaves which CANNOT exist) + +// // bfs build query... +// // execute query + +// // start at nodeIdx: doc_viewer +// // expand until I find all leaf nodes. +// // we are interested in leaf nodes with nodeIdx: user +// // return all leaf nodeIdx that match the filter. If no filter provided, return all leaf idx. + +// // TODO: optionally build the expansion path by pushing every intermediate step as the bfs is walked. + +// // with each step of the bfs, perform the operator. +// // e.g. if first step is doc_viewer -> user with operator OR then we have leaf user with operator chain [OR] +// // the next step might be doc_viewer -> doc_editor with step OR and doc_editor -> user with step OR, so we have leaf user with chain [OR, OR] +// // the next step might be doc_viewer -> doc_auditor with OR +// // then doc_auditor -> doc_editor with OR +// // then doc_editor -> doc_user with OR +// // then doc_editor -> doc_owner +// // then doc_auditor -> doc_owner with Difference +// // + +// todo!() +// } +// } + +// #[cfg(test)] +// mod test { +// use super::*; + +// #[test] +// fn build_graph_test() { +// let mut g = RelationGraph::new(); +// let user = g.add_node(1, true); +// let role = g.add_node(2, true); +// let group = g.add_node(3, true); +// let doc = g.add_node(4, true); + +// let role_member = g.add_node(5, false); + +// let group_member = g.add_node(6, false); +// let group_admin = g.add_node(7, false); + +// let doc_owner = g.add_node(8, false); +// let doc_editor = g.add_node(9, false); +// let doc_viewer = g.add_node(10, false); +// let doc_auditor = g.add_node(11, false); +// let doc_parent = g.add_node(12, false); + +// g.add_edge(role, role_member, None, None); +// g.add_edge(role_member, user, Some(Operator::Or), None); +// g.add_edge(role_member, group_member, Some(Operator::Or), None); + +// g.add_edge(group, group_member, None, None); +// g.add_edge(group, group_admin, None, None); + +// g.add_edge(group_member, user, Some(Operator::Or), None); +// g.add_edge(group_member, group_admin, Some(Operator::Or), None); +// g.add_edge(group_admin, user, Some(Operator::Or), None); + +// g.add_edge(doc, doc_owner, None, None); +// g.add_edge(doc, doc_editor, None, None); +// g.add_edge(doc, doc_viewer, None, None); +// g.add_edge(doc, doc_auditor, None, None); +// g.add_edge(doc, doc_parent, None, None); + +// g.add_edge(doc_owner, user, Some(Operator::Or), None); +// g.add_edge(doc_editor, user, Some(Operator::Or), None); +// g.add_edge(doc_editor, doc_owner, Some(Operator::Or), None); +// g.add_edge(doc_viewer, user, Some(Operator::Or), None); +// g.add_edge(doc_viewer, doc_editor, Some(Operator::Or), None); +// g.add_edge(doc_viewer, doc_parent, Some(Operator::Or), Some(doc_viewer)); +// g.add_edge(doc_viewer, user, Some(Operator::OrAll), None); +// g.add_edge(doc_viewer, doc_auditor, Some(Operator::Or), None); +// g.add_edge(doc_auditor, doc_editor, Some(Operator::Difference), None); + +// g.find_leaves(doc_viewer, None); +// assert_eq!(1, 2); +// } +// } diff --git a/crates/secd/src/auth/z/mod.rs b/crates/secd/src/auth/z/mod.rs index b364583..d663e65 100644 --- a/crates/secd/src/auth/z/mod.rs +++ b/crates/secd/src/auth/z/mod.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] // TODO: Remove when implemented +#![allow(unused_variables)] mod graph; use crate::{Authorization, Secd, SecdError}; @@ -50,7 +52,7 @@ impl Authorization for Secd { // they are "relationships" rather than what spice calls permissions spice .write_relationship( - &ts.into_iter() + &ts.iter() .map(|r| Relationship { subject: r.subject.clone(), object: r.object.clone(), |
