From eb92f823c31a5e702af7005231f0d6915aad3342 Mon Sep 17 00:00:00 2001 From: benj Date: Mon, 24 Apr 2023 13:24:45 -0700 Subject: email templates, sendgrid, creds, and some experimental things Started playing with namespace configs and integrating with zanzibar impls. Still lot's of experimenting and dead code going on. --- crates/secd/src/auth/n.rs | 166 ++++++++++++++++++++++++++----- crates/secd/src/auth/z.rs | 61 ------------ crates/secd/src/auth/z/graph.rs | 209 ++++++++++++++++++++++++++++++++++++++++ crates/secd/src/auth/z/mod.rs | 88 +++++++++++++++++ 4 files changed, 440 insertions(+), 84 deletions(-) delete mode 100644 crates/secd/src/auth/z.rs create mode 100644 crates/secd/src/auth/z/graph.rs create mode 100644 crates/secd/src/auth/z/mod.rs (limited to 'crates/secd/src/auth') diff --git a/crates/secd/src/auth/n.rs b/crates/secd/src/auth/n.rs index 1d3b2d5..1f32fd6 100644 --- a/crates/secd/src/auth/n.rs +++ b/crates/secd/src/auth/n.rs @@ -1,20 +1,23 @@ -use std::str::FromStr; - use crate::{ client::{ - email::{EmailValidationMessage, Sendable}, + email::{ + parse_email_template, EmailValidationMessage, Sendable, DEFAULT_SIGNIN_EMAIL, + DEFAULT_SIGNUP_EMAIL, + }, store::{ - AddressLens, AddressValidationLens, IdentityLens, SessionLens, Storable, StoreError, + AddressLens, AddressValidationLens, CredentialLens, IdentityLens, SessionLens, + Storable, StoreError, }, }, util, Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod, - Credential, CredentialType, Identity, PhoneNumber, Secd, SecdError, Session, SessionToken, + Credential, CredentialType, Identity, IdentityId, Secd, SecdError, Session, SessionToken, ADDRESSS_VALIDATION_CODE_SIZE, ADDRESS_VALIDATION_ALLOWS_ATTEMPTS, ADDRESS_VALIDATION_IDENTITY_SURJECTION, EMAIL_VALIDATION_DURATION, }; use email_address::EmailAddress; use log::warn; use rand::Rng; +use std::str::FromStr; use time::{Duration, OffsetDateTime}; use uuid::Uuid; @@ -22,10 +25,15 @@ impl Secd { pub async fn validate_email( &self, email_address: &str, - identity_id: Option, + identity_id: Option, ) -> Result { let email_address = EmailAddress::from_str(email_address)?; - // record address (idempotent operation) + let mut email_template = self + .cfg + .email_signup_message + .clone() + .unwrap_or(DEFAULT_SIGNUP_EMAIL.into()); + let mut address = Address { id: Uuid::new_v4(), t: AddressType::Email { @@ -50,6 +58,12 @@ impl Secd { .into_iter() .next() .ok_or(SecdError::AddressValidationFailed)?; + + email_template = self + .cfg + .email_signin_message + .clone() + .unwrap_or(DEFAULT_SIGNIN_EMAIL.into()); } let secret = hex::encode(rand::thread_rng().gen::<[u8; 32]>()); @@ -76,13 +90,23 @@ impl Secd { validation.write(self.store.clone()).await?; - let msg =EmailValidationMessage { + let msg = EmailValidationMessage { + from_address: self + .cfg + .email_address_from + .clone() + .unwrap_or("SecD ".parse().unwrap()), + replyto_address: self + .cfg + .email_address_replyto + .clone() + .unwrap_or("SecD ".parse().unwrap()), recipient: email_address.clone(), - subject: "Confirm Your Email".into(), - body: format!("This is an email validation message. Click this link [{:?}?s={}] or use the code [{}]", validation.id, secret, code), + subject: "Login Request".into(), + body: parse_email_template(&email_template, validation.id, Some(secret), Some(code))?, }; - match msg.send().await { + match msg.send(self.email_messenger.clone()).await { Ok(_) => { /* TODO: Write down the message*/ } Err(e) => { validation.revoked_at = Some(OffsetDateTime::now_utc()); @@ -95,10 +119,11 @@ impl Secd { } pub async fn validate_sms( &self, - phone_number: &PhoneNumber, + // phone_number: &PhoneNumber, ) -> Result { todo!() } + pub async fn complete_address_validation( &self, validation_id: &AddressValidationId, @@ -215,21 +240,83 @@ impl Secd { Ok(session) } + pub async fn create_credential( &self, t: CredentialType, - key: String, - value: Option, - ) -> Result { - todo!() + identity_id: Option, + ) -> Result { + let 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) + .ok_or(SecdError::IdentityNotFound)?, + + None => { + let id = Identity { + id: Uuid::new_v4(), + address_validations: vec![], + credentials: vec![], + rules: vec![], + metadata: None, + created_at: OffsetDateTime::now_utc(), + deleted_at: None, + }; + id.write(self.store.clone()).await?; + id + } + }; + + let mut credential = match &Credential::find( + self.store.clone(), + &CredentialLens { + id: None, + identity_id: Some(identity.id), + t: Some(&t), + restrict_by_key: Some(false), + }, + ) + .await?[..] + { + [] => Credential { + id: Uuid::new_v4(), + identity_id: identity.id, + t, + created_at: OffsetDateTime::now_utc(), + revoked_at: None, + deleted_at: None, + }, + _ => return Err(SecdError::CredentialAlreadyExists), + }; + + credential.hash(&self.crypter)?; + credential + .write(self.store.clone()) + .await + .map_err(|err| match err { + StoreError::IdempotentCheckAlreadyExists => SecdError::CredentialAlreadyExists, + err => SecdError::StoreError(err), + })?; + + Ok(identity) } pub async fn validate_credential( &self, - t: CredentialType, - key: String, - value: Option, + // t: CredentialType, + // key: String, + // value: Option, ) -> Result { + // Credential::find(store, lens) use key here as unique index todo!() } @@ -254,15 +341,23 @@ impl Secd { } } - pub async fn get_identity(&self, i: &SessionToken) -> Result { - let token_hash = util::hash(&hex::decode(i)?); + pub async fn get_identity( + &self, + i: Option, + t: Option, + ) -> Result { + let token_hash = match t { + Some(tok) => Some(util::hash(&hex::decode(&tok)?)), + None => None, + }; + let mut i = Identity::find( self.store.clone(), &IdentityLens { - id: None, + id: i.as_ref(), address_type: None, validated_address: None, - session_token_hash: Some(token_hash), + session_token_hash: token_hash, }, ) .await?; @@ -279,6 +374,31 @@ impl Secd { } } + pub async fn update_identity_metadata( + &self, + i: IdentityId, + md: String, + ) -> Result { + let mut identity = Identity::find( + self.store.clone(), + &IdentityLens { + id: Some(&i), + address_type: None, + validated_address: None, + session_token_hash: None, + }, + ) + .await? + .into_iter() + .nth(0) + .ok_or(SecdError::IdentityNotFound)?; + + identity.metadata = Some(md); + identity.write(self.store.clone()).await?; + + 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?; diff --git a/crates/secd/src/auth/z.rs b/crates/secd/src/auth/z.rs deleted file mode 100644 index 31f449c..0000000 --- a/crates/secd/src/auth/z.rs +++ /dev/null @@ -1,61 +0,0 @@ -use uuid::Uuid; - -use crate::{Secd, SecdError}; - -pub type Namespace = String; -pub type Object = (Namespace, Uuid); -pub type Relation = String; - -pub struct Relationship { - pub subject: Subject, - pub object: Object, - pub relation: Relation, -} - -#[derive(Clone)] -pub enum Subject { - User(Object), - UserSet { user: Object, relation: Relation }, -} - -impl Secd { - pub async fn check(&self, r: &Relationship) -> Result { - let spice = self - .spice - .clone() - .expect("TODO: only supports postgres right now"); - - Ok(spice.check_permission(r).await?) - } - pub async fn expand(&self) -> Result<(), SecdError> { - todo!() - } - pub async fn read(&self) -> Result<(), SecdError> { - todo!() - } - pub async fn watch(&self) -> Result<(), SecdError> { - unimplemented!() - } - pub async fn write(&self, ts: &[Relationship]) -> Result<(), SecdError> { - let spice = self - .spice - .clone() - .expect("TODO: only supports postgres right now"); - - // Since spice doesn't really have a great schema pattern, we - // prefix all incoming write relationships with an r_ to indicate - // they are "relationships" rather than what spice calls permissions - spice - .write_relationship( - &ts.into_iter() - .map(|r| Relationship { - subject: r.subject.clone(), - object: r.object.clone(), - relation: format!("r_{}", r.relation), - }) - .collect::>(), - ) - .await?; - Ok(()) - } -} diff --git a/crates/secd/src/auth/z/graph.rs b/crates/secd/src/auth/z/graph.rs new file mode 100644 index 0000000..9ca045b --- /dev/null +++ b/crates/secd/src/auth/z/graph.rs @@ -0,0 +1,209 @@ +use std::collections::{HashSet, VecDeque}; + +type NodeIdx = usize; +type EdgeIdx = usize; + +struct RelationGraph { + nodes: Vec, + edges: Vec, +} + +#[derive(Hash, PartialEq, Eq)] +struct Node { + id: u64, + is_namespace: bool, + edge_list_head: Option, +} + +struct Edge { + dst: NodeIdx, + op: Option, + next_edge: Option, +} + +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, + follow: Option, + ) { + 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) -> Vec { + 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); + } +} diff --git a/crates/secd/src/auth/z/mod.rs b/crates/secd/src/auth/z/mod.rs new file mode 100644 index 0000000..b364583 --- /dev/null +++ b/crates/secd/src/auth/z/mod.rs @@ -0,0 +1,88 @@ +mod graph; + +use crate::{Authorization, Secd, SecdError}; +use async_trait::async_trait; +use uuid::Uuid; + +pub type Namespace = String; +pub type Object = (Namespace, Uuid); +pub type Relation = String; + +pub struct Relationship { + pub subject: Subject, + pub object: Object, + pub relation: Relation, +} + +#[derive(Clone)] +pub enum Subject { + User(Object), + UserSet { user: Object, relation: Relation }, +} + +#[async_trait] +impl Authorization for Secd { + async fn check(&self, r: &Relationship) -> Result { + let spice = self + .spice + .clone() + .expect("TODO: only supports postgres right now"); + + Ok(spice.check_permission(r).await?) + } + async fn expand(&self) -> Result<(), SecdError> { + todo!() + } + async fn read(&self) -> Result<(), SecdError> { + todo!() + } + async fn watch(&self) -> Result<(), SecdError> { + unimplemented!() + } + async fn write(&self, ts: &[Relationship]) -> Result<(), SecdError> { + let spice = self + .spice + .clone() + .expect("TODO: only supports postgres right now"); + + // Since spice doesn't really have a great schema pattern, we + // prefix all incoming write relationships with an r_ to indicate + // they are "relationships" rather than what spice calls permissions + spice + .write_relationship( + &ts.into_iter() + .map(|r| Relationship { + subject: r.subject.clone(), + object: r.object.clone(), + relation: format!("r_{}", r.relation), + }) + .collect::>(), + ) + .await?; + Ok(()) + } +} + +enum RelationToken { + Start, + Or, + And, + Exclude, +} +struct RelationContainer { + name: Relation, + bins: Vec<(RelationToken, Relation)>, +} + +struct NamespaceContainer { + relations: Vec, +} + +impl Secd { + async fn write_namespace(&self, ns: &NamespaceContainer) -> Result<(), SecdError> { + todo!() + } + async fn read_namespace(&self) -> Result { + todo!() + } +} -- cgit v1.2.3