aboutsummaryrefslogtreecommitdiff
path: root/crates/secd
diff options
context:
space:
mode:
Diffstat (limited to 'crates/secd')
-rw-r--r--crates/secd/Cargo.toml8
-rw-r--r--crates/secd/README.md57
-rw-r--r--crates/secd/assets/default_login_email.html0
-rw-r--r--crates/secd/assets/default_signup_email.html0
-rw-r--r--crates/secd/assets/default_sms_login.txt0
-rw-r--r--crates/secd/assets/default_sms_signup.txt0
-rw-r--r--crates/secd/src/auth/n.rs166
-rw-r--r--crates/secd/src/auth/z/graph.rs209
-rw-r--r--crates/secd/src/auth/z/mod.rs (renamed from crates/secd/src/auth/z.rs)43
-rw-r--r--crates/secd/src/client/email/mod.rs186
-rw-r--r--crates/secd/src/client/store/mod.rs78
-rw-r--r--crates/secd/src/client/store/sql_db.rs132
-rw-r--r--crates/secd/src/lib.rs183
-rw-r--r--crates/secd/src/util/crypter.rs87
-rw-r--r--crates/secd/src/util/from.rs5
-rw-r--r--crates/secd/src/util/mod.rs107
-rw-r--r--crates/secd/store/pg/migrations/20221222002434_bootstrap.sql28
-rw-r--r--crates/secd/store/pg/sql/find_credential.sql12
-rw-r--r--crates/secd/store/pg/sql/write_credential.sql19
-rw-r--r--crates/secd/store/pg/sql/write_identity.sql2
-rw-r--r--crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql8
-rw-r--r--crates/secd/store/sqlite/sql/find_credential.sql12
-rw-r--r--crates/secd/store/sqlite/sql/write_credential.sql19
23 files changed, 1197 insertions, 164 deletions
diff --git a/crates/secd/Cargo.toml b/crates/secd/Cargo.toml
index 1eb30b1..94ae43c 100644
--- a/crates/secd/Cargo.toml
+++ b/crates/secd/Cargo.toml
@@ -3,10 +3,12 @@ name = "secd"
version = "0.1.0"
edition = "2021"
+
[dependencies]
+aes-gcm = "0.10.1"
+argon2 = "0.4.1"
async-trait = "0.1.59"
anyhow = "1.0"
-base64 = "0.13.1"
clap = { version = "4.0.29", features = ["derive"] }
derive_more = "0.99"
email_address = "0.2"
@@ -15,17 +17,19 @@ lazy_static = "1.4"
lettre = "0.10.1"
log = "0.4"
openssl = "0.10.42"
+pest = "2.5.2"
prost = "0.9"
prost-types = "0.9.0"
rand = "0.8"
reqwest = { version = "0.11.13", features = ["json"] }
+sendgrid = { version = "0.18", features = ["async"] }
serde = "1"
serde_json = { version = "1.0", features = ["raw_value"] }
serde_with = { version = "2.1", features = ["hex"] }
sha2 = "0.10.6"
strum = "0.24.1"
strum_macros = "0.24"
-sqlx = { path = "../../../sqlx", features = [ "runtime-async-std-native-tls", "postgres", "uuid", "sqlite", "time" ] }
+sqlx = { git = "https://github.com/benjbellon/sqlx.git", branch = "bc/safe-schemas", features = [ "runtime-async-std-native-tls", "postgres", "uuid", "sqlite", "time" ] }
time = { version = "0.3", features = [ "serde" ] }
thiserror = "1.0"
tokio = { version = "1.23.0", feautres = ["rt", "macros"] }
diff --git a/crates/secd/README.md b/crates/secd/README.md
index 5786d0c..17c333d 100644
--- a/crates/secd/README.md
+++ b/crates/secd/README.md
@@ -52,3 +52,60 @@ GET /oidc/provider?state=123444 -- state validated by client
POST /api/auth/oidc { data ... }
motif = start_motif(Oidc, access_token, data)
session = complete_motif(motif.id)
+
+ref = secd.write(User(user, 1), (doc, 3), "editor");
+secd.attach_computed_property(ref, "property_name");
+
+secd.check(User(user, 1), (doc, 3), "editor")
+secd.compute\_check(User(user, 1), (doc, 3), "editor", ["property", args...], ["property", args...])
+e.g.
+secd.compute\_check("User(user, 1), (doc, 3), "editor", ["readable_row", 2134], ["property2", args...])
+
+.....NO: A computed property should just be a domain things, and if any data is needed, it can be attached to the auth store for that identity!!!!!!!!!!!!!!!!
+
+## Namespace stuff...
+
+use file/path/1
+use file/path/2
+
+namespace user { }
+namespace role {
+ relation member {
+ user | group#member
+ }
+
+ computed_property (t: timestamp, s: timestamp) {
+ perform a computation here...
+ }
+
+ computed_property (s: string) {
+ s.starts_with("b")
+ }
+}
+
+namespace group {
+ relation member {
+ user | this.admin
+ }
+ relation admin {
+ user
+ }
+}
+
+namespace doc {
+ relation owner { user }
+ relation editor { user | this.owner }
+ relation viewer { user | this.editor | this.parent#viewer | all(user) }
+ relation auditor { editor - this.owner }
+ relation parent { this.doc }
+}
+
+so, basically it's just:
+
+namespace N {
+ relation R {
+ N#R | N#R & N#R - N#R
+ }
+}
+
+These are `.iam` files. Any `.iam` file can be specified as the main file, and then each use statement will be followed.
diff --git a/crates/secd/assets/default_login_email.html b/crates/secd/assets/default_login_email.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/crates/secd/assets/default_login_email.html
diff --git a/crates/secd/assets/default_signup_email.html b/crates/secd/assets/default_signup_email.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/crates/secd/assets/default_signup_email.html
diff --git a/crates/secd/assets/default_sms_login.txt b/crates/secd/assets/default_sms_login.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/crates/secd/assets/default_sms_login.txt
diff --git a/crates/secd/assets/default_sms_signup.txt b/crates/secd/assets/default_sms_signup.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/crates/secd/assets/default_sms_signup.txt
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<Uuid>,
+ identity_id: Option<IdentityId>,
) -> Result<AddressValidation, SecdError> {
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 <noreply@secd.com>".parse().unwrap()),
+ replyto_address: self
+ .cfg
+ .email_address_replyto
+ .clone()
+ .unwrap_or("SecD <noreply@secd.com>".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<AddressValidation, SecdError> {
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<String>,
- ) -> Result<Credential, SecdError> {
- todo!()
+ identity_id: Option<IdentityId>,
+ ) -> Result<Identity, SecdError> {
+ 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<String>,
+ // t: CredentialType,
+ // key: String,
+ // value: Option<String>,
) -> Result<Session, SecdError> {
+ // 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<Identity, SecdError> {
- let token_hash = util::hash(&hex::decode(i)?);
+ pub async fn get_identity(
+ &self,
+ i: Option<IdentityId>,
+ t: Option<SessionToken>,
+ ) -> Result<Identity, SecdError> {
+ 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<Identity, SecdError> {
+ 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/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<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);
+ }
+}
diff --git a/crates/secd/src/auth/z.rs b/crates/secd/src/auth/z/mod.rs
index 31f449c..b364583 100644
--- a/crates/secd/src/auth/z.rs
+++ b/crates/secd/src/auth/z/mod.rs
@@ -1,6 +1,8 @@
-use uuid::Uuid;
+mod graph;
-use crate::{Secd, SecdError};
+use crate::{Authorization, Secd, SecdError};
+use async_trait::async_trait;
+use uuid::Uuid;
pub type Namespace = String;
pub type Object = (Namespace, Uuid);
@@ -18,8 +20,9 @@ pub enum Subject {
UserSet { user: Object, relation: Relation },
}
-impl Secd {
- pub async fn check(&self, r: &Relationship) -> Result<bool, SecdError> {
+#[async_trait]
+impl Authorization for Secd {
+ async fn check(&self, r: &Relationship) -> Result<bool, SecdError> {
let spice = self
.spice
.clone()
@@ -27,16 +30,16 @@ impl Secd {
Ok(spice.check_permission(r).await?)
}
- pub async fn expand(&self) -> Result<(), SecdError> {
+ async fn expand(&self) -> Result<(), SecdError> {
todo!()
}
- pub async fn read(&self) -> Result<(), SecdError> {
+ async fn read(&self) -> Result<(), SecdError> {
todo!()
}
- pub async fn watch(&self) -> Result<(), SecdError> {
+ async fn watch(&self) -> Result<(), SecdError> {
unimplemented!()
}
- pub async fn write(&self, ts: &[Relationship]) -> Result<(), SecdError> {
+ async fn write(&self, ts: &[Relationship]) -> Result<(), SecdError> {
let spice = self
.spice
.clone()
@@ -59,3 +62,27 @@ impl Secd {
Ok(())
}
}
+
+enum RelationToken {
+ Start,
+ Or,
+ And,
+ Exclude,
+}
+struct RelationContainer {
+ name: Relation,
+ bins: Vec<(RelationToken, Relation)>,
+}
+
+struct NamespaceContainer {
+ relations: Vec<RelationContainer>,
+}
+
+impl Secd {
+ async fn write_namespace(&self, ns: &NamespaceContainer) -> Result<(), SecdError> {
+ todo!()
+ }
+ async fn read_namespace(&self) -> Result<NamespaceContainer, SecdError> {
+ todo!()
+ }
+}
diff --git a/crates/secd/src/client/email/mod.rs b/crates/secd/src/client/email/mod.rs
index 915d18c..7c7b233 100644
--- a/crates/secd/src/client/email/mod.rs
+++ b/crates/secd/src/client/email/mod.rs
@@ -1,68 +1,172 @@
+use async_trait::async_trait;
use email_address::EmailAddress;
-use lettre::Transport;
-use log::error;
-use std::collections::HashMap;
+use lettre::{
+ message::{Mailbox, MultiPart},
+ Transport,
+};
+use log::{error, warn};
+use reqwest::StatusCode;
+use sendgrid::v3::{Content, Email, Personalization};
+use std::sync::Arc;
+
+use crate::AddressValidationId;
+
+pub const DEFAULT_SIGNUP_EMAIL: &str = "This email was recently used to signup. Please use the following code to complete your validation: {{secd::validation_code}}. If you did not request this signup link, you can safely ignore this email.";
+pub const DEFAULT_SIGNIN_EMAIL: &str = "An account with this email was recently used to signin. Please use the following code to complete your sign in process: {{secd::validation_code}}. If you did not request this signin link, you can safely ingore this email.";
#[derive(Debug, thiserror::Error, derive_more::Display)]
pub enum EmailMessengerError {
FailedToSendEmail,
+ LibLettreError(#[from] lettre::error::Error),
+ SendgridError(#[from] sendgrid::SendgridError),
}
pub struct EmailValidationMessage {
+ pub from_address: Mailbox,
+ pub replyto_address: Mailbox,
pub recipient: EmailAddress,
pub subject: String,
pub body: String,
}
-#[async_trait::async_trait]
-pub(crate) trait EmailMessenger {
- async fn send_email(
- &self,
- email_address: &EmailAddress,
- template: &str,
- template_vars: HashMap<&str, &str>,
- ) -> Result<(), EmailMessengerError>;
+#[async_trait]
+pub(crate) trait EmailMessenger: Send + Sync {
+ fn get_type(&self) -> MessengerType;
+ fn get_api_key(&self) -> String;
}
-pub(crate) struct LocalMailer {}
+pub enum MessengerType {
+ LocalMailer,
+ Sendgrid,
+}
-#[async_trait::async_trait]
+pub(crate) struct LocalMailer {}
+impl LocalMailer {
+ pub fn new() -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
+ warn!("You are using the local mailer, which will not work in production!");
+ Arc::new(LocalMailer {})
+ }
+}
impl EmailMessenger for LocalMailer {
- async fn send_email(
- &self,
- email_address: &EmailAddress,
- template: &str,
- template_vars: HashMap<&str, &str>,
- ) -> Result<(), EmailMessengerError> {
- todo!()
+ fn get_type(&self) -> MessengerType {
+ MessengerType::LocalMailer
+ }
+ fn get_api_key(&self) -> String {
+ panic!("unreachable since no API key is expected for LocalMailer");
+ }
+}
+pub(crate) struct Sendgrid {
+ pub api_key: String,
+}
+impl Sendgrid {
+ pub fn new(api_key: String) -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
+ Arc::new(Sendgrid { api_key })
+ }
+}
+impl EmailMessenger for Sendgrid {
+ fn get_type(&self) -> MessengerType {
+ MessengerType::Sendgrid
+ }
+ fn get_api_key(&self) -> String {
+ self.api_key.clone()
}
}
-#[async_trait::async_trait]
+#[async_trait]
pub(crate) trait Sendable {
- async fn send(&self) -> Result<(), EmailMessengerError>;
+ async fn send(&self, messenge: Arc<dyn EmailMessenger>) -> Result<(), EmailMessengerError>;
}
-#[async_trait::async_trait]
+#[async_trait]
impl Sendable for EmailValidationMessage {
- // TODO: We need to break this up as before, especially so we can feature
- // gate unwanted things like Lettre...
- async fn send(&self) -> Result<(), EmailMessengerError> {
- // TODO: Get these things from the template...
- let email = lettre::Message::builder()
- .from("BranchControl <iam@branchcontrol.com>".parse().unwrap())
- .reply_to("BranchControl <iam@branchcontrol.com>".parse().unwrap())
- .to(self.recipient.to_string().parse().unwrap())
- .subject(self.subject.clone())
- .body(self.body.clone())
- .unwrap();
-
- let mailer = lettre::SmtpTransport::unencrypted_localhost();
-
- mailer.send(&email).map_err(|e| {
- error!("failed to send email {:?}", e);
- EmailMessengerError::FailedToSendEmail
- })?;
+ async fn send(&self, messenger: Arc<dyn EmailMessenger>) -> Result<(), EmailMessengerError> {
+ match messenger.get_type() {
+ MessengerType::LocalMailer => {
+ let email = lettre::Message::builder()
+ .from(self.from_address.to_string().parse().unwrap())
+ .reply_to(self.replyto_address.to_string().parse().unwrap())
+ .to(self.recipient.to_string().parse().unwrap())
+ .subject(self.subject.clone())
+ .multipart(MultiPart::alternative_plain_html(
+ "".to_string(),
+ String::from(self.body.clone()),
+ ))?;
+
+ let mailer = lettre::SmtpTransport::unencrypted_localhost();
+
+ mailer.send(&email).map_err(|e| {
+ error!("failed to send email {:?}", e);
+ EmailMessengerError::FailedToSendEmail
+ })?;
+ }
+ MessengerType::Sendgrid => {
+ let msg = sendgrid::v3::Message::new(Email::new(self.from_address.to_string()))
+ .set_subject(&self.subject)
+ .add_content(
+ Content::new()
+ .set_content_type("text/html")
+ .set_value(&self.body),
+ )
+ .add_personalization(Personalization::new(Email::new(
+ self.recipient.to_string(),
+ )));
+
+ let sender = sendgrid::v3::Sender::new(messenger.get_api_key());
+ let resp = sender.send(&msg).await?;
+ match resp.status() {
+ StatusCode::ACCEPTED => {}
+ _ => {
+ error!(
+ "sendgrid failed to send message with status: {}",
+ resp.status()
+ )
+ }
+ }
+ }
+ };
+
Ok(())
}
}
+
+pub(crate) fn parse_email_template(
+ template: &str,
+ validation_id: AddressValidationId,
+ validation_secret: Option<String>,
+ validation_code: Option<String>,
+) -> Result<String, EmailMessengerError> {
+ let mut t = template.clone().to_string();
+ // We do not allow substutions for a variety of reasons, but mainly security ones.
+ // The only things we want to share are those which secd allows. In this case, that
+ // means we only send an email with static content as provided by the filter, except
+ // for the $$secd:request_id$$ and $$secd:request_code$$, either of which may be
+ // present in the email.
+
+ t = t.replace("{{secd::validation_id}}", &validation_id.to_string());
+ validation_secret.map(|secret| t = t.replace("{{secd::validation_secret}}", &secret));
+ validation_code.map(|code| t = t.replace("{{secd::validation_code}}", &code));
+
+ Ok(t)
+}
+
+#[cfg(test)]
+mod test {
+ use uuid::Uuid;
+
+ use super::*;
+
+ #[test]
+ fn test_parse_and_substitue() {
+ let raw = "This is an email validation message. Navigate to https://www.secd.com/auth/{secd::validation_id}?s={secd::validation_secret} or use the code [{secd::validation_code}]";
+
+ let parsed = parse_email_template(
+ raw,
+ Uuid::parse_str("90f42ba9-ed4a-4f56-b371-df05634a1626").unwrap(),
+ Some("s3cr3t".into()),
+ Some("102030".into()),
+ )
+ .unwrap();
+
+ assert_eq!(parsed, "This is an email validation message. Navigate to https://www.secd.com/auth/90f42ba9-ed4a-4f56-b371-df05634a1626?s=s3cr3t or use the code [102030]")
+ }
+}
diff --git a/crates/secd/src/client/store/mod.rs b/crates/secd/src/client/store/mod.rs
index 8a076c4..7bf01d5 100644
--- a/crates/secd/src/client/store/mod.rs
+++ b/crates/secd/src/client/store/mod.rs
@@ -1,22 +1,28 @@
pub(crate) mod sql_db;
+use async_trait::async_trait;
use sqlx::{Postgres, Sqlite};
use std::sync::Arc;
use uuid::Uuid;
-use crate::{util, Address, AddressType, AddressValidation, Identity, IdentityId, Session};
+use crate::{
+ util, Address, AddressType, AddressValidation, Credential, CredentialId, CredentialType,
+ Identity, IdentityId, Session,
+};
use self::sql_db::SqlClient;
#[derive(Debug, thiserror::Error, derive_more::Display)]
pub enum StoreError {
SqlClientError(#[from] sqlx::Error),
+ SerdeError(#[from] serde_json::Error),
+ ParseError(#[from] strum::ParseError),
StoreValueCannotBeParsedInvariant,
IdempotentCheckAlreadyExists,
}
-#[async_trait::async_trait(?Send)]
-pub trait Store {
+#[async_trait]
+pub trait Store: Send + Sync {
fn get_type(&self) -> StoreType;
}
@@ -25,7 +31,7 @@ pub enum StoreType {
Sqlite { c: Arc<SqlClient<Sqlite>> },
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
pub(crate) trait Storable<'a> {
type Item;
type Lens;
@@ -64,7 +70,15 @@ pub(crate) struct SessionLens<'a> {
}
impl<'a> Lens for SessionLens<'a> {}
-#[async_trait::async_trait(?Send)]
+pub(crate) struct CredentialLens<'a> {
+ pub id: Option<CredentialId>,
+ pub identity_id: Option<IdentityId>,
+ pub t: Option<&'a CredentialType>,
+ pub restrict_by_key: Option<bool>,
+}
+impl<'a> Lens for CredentialLens<'a> {}
+
+#[async_trait]
impl<'a> Storable<'a> for Address {
type Item = Address;
type Lens = AddressLens<'a>;
@@ -93,7 +107,7 @@ impl<'a> Storable<'a> for Address {
}
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
impl<'a> Storable<'a> for AddressValidation {
type Item = AddressValidation;
type Lens = AddressValidationLens<'a>;
@@ -116,7 +130,7 @@ impl<'a> Storable<'a> for AddressValidation {
}
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
impl<'a> Storable<'a> for Identity {
type Item = Identity;
type Lens = IdentityLens<'a>;
@@ -158,7 +172,7 @@ impl<'a> Storable<'a> for Identity {
}
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
impl<'a> Storable<'a> for Session {
type Item = Session;
type Lens = SessionLens<'a>;
@@ -183,3 +197,51 @@ impl<'a> Storable<'a> for Session {
})
}
}
+
+#[async_trait]
+impl<'a> Storable<'a> for Credential {
+ type Item = Credential;
+ type Lens = CredentialLens<'a>;
+
+ async fn write(&self, store: Arc<dyn Store>) -> Result<(), StoreError> {
+ match store.get_type() {
+ StoreType::Postgres { c } => c.write_credential(self).await?,
+ StoreType::Sqlite { c } => c.write_credential(self).await?,
+ }
+ Ok(())
+ }
+
+ async fn find(
+ store: Arc<dyn Store>,
+ lens: &'a Self::Lens,
+ ) -> Result<Vec<Self::Item>, StoreError> {
+ Ok(match store.get_type() {
+ StoreType::Postgres { c } => {
+ c.find_credential(
+ lens.id,
+ lens.identity_id,
+ lens.t,
+ if let Some(true) = lens.restrict_by_key {
+ true
+ } else {
+ false
+ },
+ )
+ .await?
+ }
+ StoreType::Sqlite { c } => {
+ c.find_credential(
+ lens.id,
+ lens.identity_id,
+ lens.t,
+ if let Some(true) = lens.restrict_by_key {
+ true
+ } else {
+ false
+ },
+ )
+ .await?
+ }
+ })
+ }
+}
diff --git a/crates/secd/src/client/store/sql_db.rs b/crates/secd/src/client/store/sql_db.rs
index ecb13be..3e72fe8 100644
--- a/crates/secd/src/client/store/sql_db.rs
+++ b/crates/secd/src/client/store/sql_db.rs
@@ -1,27 +1,19 @@
-use std::{str::FromStr, sync::Arc};
-
-use email_address::EmailAddress;
-use serde_json::value::RawValue;
-use sqlx::{
- database::HasArguments, types::Json, ColumnIndex, Database, Decode, Encode, Executor,
- IntoArguments, Pool, Transaction, Type,
-};
-use time::OffsetDateTime;
-use uuid::Uuid;
-
+use super::{Store, StoreError, StoreType};
use crate::{
- Address, AddressType, AddressValidation, AddressValidationMethod, Identity, Session,
- SessionToken,
+ Address, AddressType, AddressValidation, AddressValidationMethod, Credential, CredentialId,
+ CredentialType, Identity, IdentityId, Session,
};
-
+use email_address::EmailAddress;
use lazy_static::lazy_static;
+use sqlx::{
+ database::HasArguments, ColumnIndex, Database, Decode, Encode, Executor, IntoArguments, Pool,
+ Transaction, Type,
+};
use sqlx::{Postgres, Sqlite};
use std::collections::HashMap;
-
-use super::{
- AddressLens, AddressValidationLens, IdentityLens, SessionLens, Storable, Store, StoreError,
- StoreType,
-};
+use std::{str::FromStr, sync::Arc};
+use time::OffsetDateTime;
+use uuid::Uuid;
const SQLITE: &str = "sqlite";
const PGSQL: &str = "pgsql";
@@ -30,6 +22,8 @@ const WRITE_ADDRESS: &str = "write_address";
const FIND_ADDRESS: &str = "find_address";
const WRITE_ADDRESS_VALIDATION: &str = "write_address_validation";
const FIND_ADDRESS_VALIDATION: &str = "find_address_validation";
+const WRITE_CREDENTIAL: &str = "write_credential";
+const FIND_CREDENTIAL: &str = "find_credential";
const WRITE_IDENTITY: &str = "write_identity";
const FIND_IDENTITY: &str = "find_identity";
const WRITE_SESSION: &str = "write_session";
@@ -72,6 +66,14 @@ lazy_static! {
FIND_SESSION,
include_str!("../../../store/sqlite/sql/find_session.sql"),
),
+ (
+ WRITE_CREDENTIAL,
+ include_str!("../../../store/sqlite/sql/write_credential.sql"),
+ ),
+ (
+ FIND_CREDENTIAL,
+ include_str!("../../../store/sqlite/sql/find_credential.sql"),
+ ),
]
.iter()
.cloned()
@@ -110,6 +112,14 @@ lazy_static! {
FIND_SESSION,
include_str!("../../../store/pg/sql/find_session.sql"),
),
+ (
+ WRITE_CREDENTIAL,
+ include_str!("../../../store/pg/sql/write_credential.sql"),
+ ),
+ (
+ FIND_CREDENTIAL,
+ include_str!("../../../store/pg/sql/find_credential.sql"),
+ ),
]
.iter()
.cloned()
@@ -131,7 +141,7 @@ pub trait SqlxResultExt<T> {
impl<T> SqlxResultExt<T> for Result<T, sqlx::Error> {
fn extend_err(self) -> Result<T, StoreError> {
if let Err(sqlx::Error::Database(dbe)) = &self {
- if dbe.code() == Some("23505".into()) {
+ if dbe.code() == Some("23505".into()) || dbe.code() == Some("2067".into()) {
return Err(StoreError::IdempotentCheckAlreadyExists);
}
}
@@ -160,7 +170,7 @@ impl Store for PgClient {
impl PgClient {
pub async fn new(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> {
- sqlx::migrate!("store/pg/migrations")
+ sqlx::migrate!("store/pg/migrations", "secd")
.run(&pool)
.await
.expect(ERR_MSG_MIGRATION_FAILED);
@@ -187,7 +197,7 @@ impl Store for SqliteClient {
impl SqliteClient {
pub async fn new(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> {
- sqlx::migrate!("store/sqlite/migrations")
+ sqlx::migrate!("store/sqlite/migrations", "secd")
.run(&pool)
.await
.expect(ERR_MSG_MIGRATION_FAILED);
@@ -410,8 +420,7 @@ where
let sqls = get_sqls(&self.sqls_root, WRITE_IDENTITY);
sqlx::query(&sqls[0])
.bind(i.id)
- // TODO: validate this is actually Json somewhere way up the chain (when being deserialized)
- .bind(i.metadata.clone().unwrap_or("{}".into()))
+ .bind(i.metadata.clone())
.bind(i.created_at)
.bind(OffsetDateTime::now_utc())
.bind(i.deleted_at)
@@ -449,7 +458,7 @@ where
.extend_err()?;
let mut res = vec![];
- for (id, metadata, created_at, updated_at, deleted_at) in rs.into_iter() {
+ for (id, metadata, created_at, _, deleted_at) in rs.into_iter() {
res.push(Identity {
id,
address_validations: vec![],
@@ -509,6 +518,79 @@ where
}
Ok(res)
}
+
+ pub async fn write_credential(&self, c: &Credential) -> Result<(), StoreError> {
+ let sqls = get_sqls(&self.sqls_root, WRITE_CREDENTIAL);
+ let partial_key = match &c.t {
+ crate::CredentialType::Passphrase { key, value: _ } => Some(key.clone()),
+ _ => None,
+ };
+
+ sqlx::query(&sqls[0])
+ .bind(c.id)
+ .bind(c.identity_id)
+ .bind(partial_key)
+ .bind(c.t.to_string())
+ .bind(serde_json::to_string(&c.t)?)
+ .bind(c.created_at)
+ .bind(c.revoked_at)
+ .bind(c.deleted_at)
+ .execute(&self.pool)
+ .await
+ .extend_err()?;
+ Ok(())
+ }
+ pub async fn find_credential(
+ &self,
+ id: Option<Uuid>,
+ identity_id: Option<Uuid>,
+ t: Option<&CredentialType>,
+ restrict_by_key: bool,
+ ) -> Result<Vec<Credential>, StoreError> {
+ let sqls = get_sqls(&self.sqls_root, FIND_CREDENTIAL);
+ let key = restrict_by_key
+ .then(|| {
+ t.map(|i| match i {
+ CredentialType::Passphrase { key, value: _ } => key.clone(),
+ _ => todo!(),
+ })
+ })
+ .flatten();
+
+ let rs = sqlx::query_as::<
+ _,
+ (
+ CredentialId,
+ IdentityId,
+ String,
+ OffsetDateTime,
+ Option<OffsetDateTime>,
+ Option<OffsetDateTime>,
+ ),
+ >(&sqls[0])
+ .bind(id.as_ref())
+ .bind(identity_id.as_ref())
+ .bind(t.map(|i| i.to_string()))
+ .bind(key)
+ .fetch_all(&self.pool)
+ .await
+ .extend_err()?;
+
+ let mut res = vec![];
+ for (id, identity_id, data, created_at, revoked_at, deleted_at) in rs.into_iter() {
+ let t: CredentialType = serde_json::from_str(&data)?;
+ res.push(Credential {
+ id,
+ identity_id,
+ t,
+ created_at,
+ revoked_at,
+ deleted_at,
+ })
+ }
+
+ Ok(res)
+ }
}
fn get_sqls(root: &str, file: &str) -> Vec<String> {
diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs
index 15615b2..54759a5 100644
--- a/crates/secd/src/lib.rs
+++ b/crates/secd/src/lib.rs
@@ -2,8 +2,10 @@ pub mod auth;
mod client;
mod util;
+use async_trait::async_trait;
+use auth::z::Relationship;
use client::{
- email::{EmailMessenger, EmailMessengerError, LocalMailer},
+ email::{EmailMessenger, EmailMessengerError, LocalMailer, Sendgrid},
spice::{Spice, SpiceError},
store::{
sql_db::{PgClient, SqliteClient},
@@ -11,28 +13,40 @@ use client::{
},
};
use email_address::EmailAddress;
-use log::{error, info};
+use lettre::message::Mailbox;
+use log::{error, info, warn};
use serde::{Deserialize, Serialize};
-use serde_with::{serde_as, DisplayFromStr};
-use std::{env::var, str::FromStr, sync::Arc};
+use serde_with::serde_as;
+use std::{
+ env::{set_var, var},
+ str::FromStr,
+ sync::Arc,
+};
use strum_macros::{Display, EnumString, EnumVariantNames};
use time::OffsetDateTime;
-use url::Url;
+use util::crypter::{Crypter, CrypterError};
use uuid::Uuid;
pub const ENV_AUTH_STORE_CONN_STRING: &str = "SECD_AUTH_STORE_CONN_STRING";
+pub const ENV_CRYPTER_SECRET_KEY: &str = "SECD_CRYPTER_SECRET_KEY";
+pub const ENV_EMAIL_ADDRESS_FROM: &str = "SECD_EMAIL_ADDRESS_FROM";
+pub const ENV_EMAIL_ADDRESS_REPLYTO: &str = "SECD_EMAIL_ADDRESS_REPLYTO";
pub const ENV_EMAIL_MESSENGER: &str = "SECD_EMAIL_MESSENGER";
pub const ENV_EMAIL_MESSENGER_CLIENT_ID: &str = "SECD_EMAIL_MESSENGER_CLIENT_ID";
pub const ENV_EMAIL_MESSENGER_CLIENT_SECRET: &str = "SECD_EMAIL_MESSENGER_CLIENT_SECRET";
+pub const ENV_EMAIL_SENDGRID_API_KEY: &str = "SECD_EMAIL_SENDGRID_API_KEY";
+pub const ENV_EMAIL_SIGNIN_MESSAGE: &str = "SECD_EMAIL_SIGNIN_MESSAGE";
+pub const ENV_EMAIL_SIGNUP_MESSAGE: &str = "SECD_EMAIL_SIGNUP_MESSAGE";
pub const ENV_SPICE_SECRET: &str = "SECD_SPICE_SECRET";
pub const ENV_SPICE_SERVER: &str = "SECD_SPICE_SERVER";
-const SESSION_SIZE_BYTES: usize = 32;
-const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */;
-const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */;
const ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6;
const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5;
const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true;
+const CRYPTER_SECRET_KEY_DEFAULT: &str = "sup3rs3cr3t";
+const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */;
+const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */;
+const SESSION_SIZE_BYTES: usize = 32;
pub type AddressId = Uuid;
pub type AddressValidationId = Uuid;
@@ -45,10 +59,17 @@ pub type SessionToken = String;
#[derive(Debug, derive_more::Display, thiserror::Error)]
pub enum SecdError {
+ // AuthenticationError(AuthnError);
+ // AuthorizationError(AuthzError);
+ // // InitializationError(...)
AddressValidationFailed,
AddressValidationSessionExchangeFailed,
AddressValidationExpiredOrConsumed,
+ CredentialAlreadyExists,
+
+ CrypterError(#[from] CrypterError),
+
TooManyIdentities,
IdentityNotFound,
@@ -69,9 +90,62 @@ pub enum SecdError {
}
pub struct Secd {
- store: Arc<dyn Store + Send + Sync + 'static>,
+ crypter: Crypter,
email_messenger: Arc<dyn EmailMessenger + Send + Sync + 'static>,
spice: Option<Arc<Spice>>,
+ store: Arc<dyn Store + Send + Sync + 'static>,
+ cfg: Cfg,
+}
+
+struct Cfg {
+ email_address_from: Option<Mailbox>,
+ email_address_replyto: Option<Mailbox>,
+ email_signup_message: Option<String>,
+ email_signin_message: Option<String>,
+}
+
+#[async_trait]
+pub trait Authentication {
+ async fn validate_address(
+ &self,
+ address_type: AddressType,
+ identity_id: Option<IdentityId>,
+ ) -> Result<AddressValidation, SecdError>;
+
+ async fn complete_address_validation(
+ &self,
+ validation_id: &AddressValidationId,
+ plaintext_token: Option<String>,
+ plaintext_code: Option<String>,
+ ) -> Result<AddressValidation, SecdError>;
+
+ async fn create_credential(
+ &self,
+ t: &CredentialType,
+ identity_id: Option<IdentityId>,
+ ) -> Result<IdentityId, SecdError>;
+ // async fn update_credential(&self, t: &CredentialType) -> Result<(), SecdError>;
+ async fn reset_credential(
+ &self,
+ t: &CredentialType,
+ address: &AddressType,
+ ) -> Result<Credential, SecdError>;
+ async fn validate_credential(&self, t: &CredentialType) -> Result<Credential, SecdError>;
+
+ // async fn expire_session_chain(&self, t: &SessionToken) -> Result<(), SecdError>;
+ // async fn expire_sessions(&self, i: &IdentityId) -> Result<(), SecdError>;
+
+ // async fn get_identity(&self, t: &SessionToken) -> Result<Identity, SecdError>;
+ // async fn get_session(&self, t: &SessionToken) -> Result<Session, SecdError>;
+}
+
+#[async_trait]
+pub trait Authorization {
+ async fn check(&self, r: &Relationship) -> Result<bool, SecdError>;
+ async fn expand(&self) -> Result<(), SecdError>;
+ async fn read(&self) -> Result<(), SecdError>;
+ async fn watch(&self) -> Result<(), SecdError>;
+ async fn write(&self, relationships: &[Relationship]) -> Result<(), SecdError>;
}
#[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)]
@@ -136,34 +210,53 @@ pub enum AddressType {
Sms { phone_number: Option<PhoneNumber> },
}
+#[serde_as]
#[derive(Debug, Serialize)]
pub struct Credential {
pub id: CredentialId,
pub identity_id: IdentityId,
pub t: CredentialType,
+ #[serde(with = "time::serde::timestamp")]
+ pub created_at: OffsetDateTime,
+ #[serde(with = "time::serde::timestamp::option")]
+ pub revoked_at: Option<OffsetDateTime>,
+ #[serde(with = "time::serde::timestamp::option")]
+ pub deleted_at: Option<OffsetDateTime>,
}
#[serde_as]
-#[derive(Debug, Serialize)]
+#[derive(Debug, Display, Serialize, Deserialize, EnumString)]
pub enum CredentialType {
- Passphrase {
- key: String,
- value: String,
- },
- Oicd {
- value: String,
- },
- OneTimeCodes {
- codes: Vec<String>,
- },
- Totp {
- #[serde_as(as = "DisplayFromStr")]
- url: Url,
- code: String,
- },
- WebAuthn {
- value: String,
- },
+ Passphrase { key: String, value: String },
+ Oidc { value: String },
+ OneTimeCodes { codes: Vec<String> },
+ // Totp {
+ // #[serde_as(as = "DisplayFromStr")]
+ // url: Url,
+ // code: String,
+ // },
+ WebAuthn { value: String },
+}
+
+struct SecuredCredential {
+ pub id: CredentialId,
+ pub identity_id: IdentityId,
+ pub t: CredentialType,
+ pub created_at: OffsetDateTime,
+ pub revoked_at: Option<OffsetDateTime>,
+ pub deleted_at: Option<OffsetDateTime>,
+}
+
+enum SecuredCredentialType {
+ Passphrase { key: String, value: String },
+ Oidc { value: String },
+ OneTimeCodes { codes: Vec<String> },
+ // Totp {
+ // #[serde_as(as = "DisplayFromStr")]
+ // url: Url,
+ // code: String,
+ // },
+ WebAuthn { value: String },
}
#[serde_with::skip_serializing_none]
@@ -207,12 +300,29 @@ impl Secd {
&var(ENV_EMAIL_MESSENGER).unwrap_or(AuthEmailMessenger::Local.to_string()),
)
.expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5");
- let email_messenger_client_id = var(ENV_EMAIL_MESSENGER_CLIENT_ID).ok();
- let email_messenger_client_secret = var(ENV_EMAIL_MESSENGER_CLIENT_SECRET).ok();
+ let crypter_secret_key = var(ENV_CRYPTER_SECRET_KEY).unwrap_or_else(|_| {
+ warn!(
+ "NO CRYPTER KEY PROVIDED, USING DEFAULT KEY. DO NOT USE THIS KEY IN PRODUCTION. PROVIDE A UNIQUE SECRET KEY BY SETTING THE ENVIORNMENT VARIABLE {}. THE DEFAULT KEY IS: {}",
+ ENV_CRYPTER_SECRET_KEY,
+ CRYPTER_SECRET_KEY_DEFAULT,
+ );
+ CRYPTER_SECRET_KEY_DEFAULT.to_string()
+ });
info!("starting client with auth_store: {:?}", auth_store);
info!("starting client with email_messenger: {:?}", auth_store);
+ let cfg = Cfg {
+ email_address_from: var(ENV_EMAIL_ADDRESS_FROM)
+ .ok()
+ .and_then(|s| s.parse().ok()),
+ email_address_replyto: var(ENV_EMAIL_ADDRESS_REPLYTO)
+ .ok()
+ .and_then(|s| s.parse().ok()),
+ email_signup_message: var(ENV_EMAIL_SIGNUP_MESSAGE).ok(),
+ email_signin_message: var(ENV_EMAIL_SIGNIN_MESSAGE).ok(),
+ };
+
let store = match auth_store {
AuthStore::Sqlite { conn } => {
if z_schema.is_some() {
@@ -252,7 +362,10 @@ impl Secd {
};
let email_sender = match email_messenger {
- AuthEmailMessenger::Local => LocalMailer {},
+ AuthEmailMessenger::Local => LocalMailer::new(),
+ AuthEmailMessenger::Sendgrid => Sendgrid::new(
+ var(ENV_EMAIL_SENDGRID_API_KEY).expect("No SENDGRID_API_KEY provided"),
+ ),
_ => unimplemented!(),
};
@@ -267,10 +380,14 @@ impl Secd {
None => None,
};
+ let crypter = Crypter::new(crypter_secret_key.as_bytes());
+
Ok(Secd {
- store,
- email_messenger: Arc::new(email_sender),
+ crypter,
+ email_messenger: email_sender,
spice,
+ store,
+ cfg,
})
}
}
diff --git a/crates/secd/src/util/crypter.rs b/crates/secd/src/util/crypter.rs
new file mode 100644
index 0000000..1717377
--- /dev/null
+++ b/crates/secd/src/util/crypter.rs
@@ -0,0 +1,87 @@
+use aes_gcm::{
+ aead::{Aead, KeyInit, OsRng},
+ Aes256Gcm, Nonce,
+};
+use argon2::{
+ password_hash::{self, SaltString},
+ Argon2, PasswordHasher,
+};
+use derive_more::Display;
+use rand::Rng;
+use sha2::{Digest, Sha256};
+use thiserror::Error;
+
+#[derive(Debug, Display, Error)]
+pub enum CrypterError {
+ EncryptError(String),
+ DecryptError(String),
+ DecodeError(String),
+ HashError(String),
+}
+
+pub struct Crypter {
+ pub key: Vec<u8>,
+}
+
+impl Crypter {
+ pub fn new(key: &[u8]) -> Self {
+ Self { key: key.to_vec() }
+ }
+
+ pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, CrypterError> {
+ let mut hasher = Sha256::new();
+ hasher.update(&self.key);
+
+ let cipher = Aes256Gcm::new(&hasher.finalize());
+
+ let rbs = rand::thread_rng().gen::<[u8; 12]>();
+ let iv = Nonce::from_slice(&rbs);
+ let crypt = cipher
+ .encrypt(&iv, data)
+ .map_err(|e| CrypterError::EncryptError(e.to_string()))?;
+
+ let mut msg = iv.to_vec();
+ msg.extend_from_slice(&crypt);
+ Ok(msg)
+ }
+
+ pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, CrypterError> {
+ let mut hasher = Sha256::new();
+ hasher.update(&self.key);
+
+ let cipher = Aes256Gcm::new(&hasher.finalize());
+
+ let iv = Nonce::from_slice(&data[0..=11]);
+ let data = &data[12..];
+ Ok(cipher
+ .decrypt(&iv, data)
+ .map_err(|e| CrypterError::DecryptError(e.to_string()))?)
+ }
+
+ pub fn hash(&self, data: &[u8]) -> Result<String, CrypterError> {
+ let salt = SaltString::generate(&mut OsRng);
+ let hasher = Argon2::default();
+ Ok(hasher
+ .hash_password(data, &salt)
+ .map_err(|e| CrypterError::HashError(e.to_string()))?
+ .to_string())
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn encrypt_data_test() {
+ let crypter = Crypter {
+ key: "testkey".to_string().into_bytes(),
+ };
+
+ let plaintext = "This is a secret.";
+ let enc = crypter.encrypt(&plaintext.as_bytes()).unwrap();
+
+ let res = crypter.decrypt(&enc).unwrap();
+ assert_eq!(plaintext, std::str::from_utf8(&res).unwrap());
+ }
+}
diff --git a/crates/secd/src/util/from.rs b/crates/secd/src/util/from.rs
index bab8a25..ec5b62d 100644
--- a/crates/secd/src/util/from.rs
+++ b/crates/secd/src/util/from.rs
@@ -1,5 +1,3 @@
-use std::str::FromStr;
-
use crate::AuthStore;
impl From<Option<String>> for AuthStore {
@@ -23,7 +21,6 @@ mod test {
#[test]
fn auth_store_from_string() {
let in_conn = None;
- let a = AuthStore::from(in_conn.clone());
match AuthStore::from(in_conn.clone()) {
AuthStore::Sqlite { conn } => assert_eq!(conn, "sqlite::memory:"),
r @ _ => assert!(
@@ -46,14 +43,12 @@ mod test {
}
let sqlite_conn = Some("sqlite:///path/to/db.sql".into());
- let a = AuthStore::from(sqlite_conn.clone());
match AuthStore::from(sqlite_conn.clone()) {
AuthStore::Sqlite { conn } => assert_eq!(conn, sqlite_conn.unwrap()),
r @ _ => assert!(false, "should have parsed as sqlite. Found: {:?}", r),
}
let sqlite_mem_conn = Some("sqlite:memory:".into());
- let a = AuthStore::from(sqlite_mem_conn.clone());
match AuthStore::from(sqlite_mem_conn.clone()) {
AuthStore::Sqlite { conn } => assert_eq!(conn, sqlite_mem_conn.unwrap()),
r @ _ => assert!(
diff --git a/crates/secd/src/util/mod.rs b/crates/secd/src/util/mod.rs
index c26986d..8676f26 100644
--- a/crates/secd/src/util/mod.rs
+++ b/crates/secd/src/util/mod.rs
@@ -1,21 +1,14 @@
+pub(crate) mod crypter;
pub(crate) mod from;
-use rand::{thread_rng, Rng};
+use self::crypter::{Crypter, CrypterError};
+use crate::{
+ AddressType, Credential, CredentialType, IdentityId, SecdError, Session, SESSION_DURATION,
+ SESSION_SIZE_BYTES,
+};
use sha2::{Digest, Sha256};
+use std::str::from_utf8;
use time::OffsetDateTime;
-use url::Url;
-
-use crate::{AddressType, IdentityId, SecdError, Session, SESSION_DURATION, SESSION_SIZE_BYTES};
-
-pub(crate) fn remove_trailing_slash(url: &mut Url) -> String {
- let mut u = url.to_string();
-
- if u.ends_with('/') {
- u.pop();
- }
-
- u
-}
pub(crate) fn hash(i: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
@@ -51,3 +44,89 @@ impl Session {
})
}
}
+
+impl Credential {
+ pub(crate) fn encrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ Ok(match self.t {
+ CredentialType::Passphrase {
+ key: _,
+ ref mut value,
+ } => {
+ *value = hex::encode(crypter.encrypt(value.as_bytes())?);
+ }
+ _ => {}
+ })
+ }
+ pub(crate) fn decrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ Ok(match self.t {
+ CredentialType::Passphrase {
+ key: _,
+ ref mut value,
+ } => {
+ *value = from_utf8(
+ &crypter.decrypt(
+ &hex::decode(value.clone())
+ .map_err(|e| CrypterError::DecodeError(e.to_string()))?,
+ )?,
+ )
+ .map_err(|e| CrypterError::DecodeError(e.to_string()))?
+ .to_string()
+ }
+ _ => {}
+ })
+ }
+
+ pub(crate) fn hash(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ Ok(match self.t {
+ CredentialType::Passphrase {
+ key: _,
+ ref mut value,
+ } => {
+ *value = crypter.hash(value.as_bytes())?;
+ }
+ _ => {}
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use uuid::Uuid;
+
+ use super::*;
+
+ #[test]
+ fn test_credential_encrypt() {
+ let c = Crypter::new("AMAZING_KEY".as_bytes());
+
+ let plaintext_secret = "super_password".to_string();
+
+ let mut credential = Credential {
+ id: Uuid::new_v4(),
+ identity_id: Uuid::new_v4(),
+ t: CredentialType::Passphrase {
+ key: "super_user".into(),
+ value: plaintext_secret.clone(),
+ },
+ created_at: OffsetDateTime::now_utc(),
+ revoked_at: None,
+ deleted_at: None,
+ };
+
+ credential.encrypt(&c).unwrap();
+ match &credential.t {
+ CredentialType::Passphrase { key: _, value } => {
+ assert_ne!(plaintext_secret.clone(), value.clone())
+ }
+ _ => {}
+ };
+
+ credential.decrypt(&c).unwrap();
+ match &credential.t {
+ CredentialType::Passphrase { key: _, value } => {
+ assert_eq!(plaintext_secret.clone(), value.clone())
+ }
+ _ => {}
+ };
+ }
+}
diff --git a/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
index 2b89957..0cf3fa0 100644
--- a/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
+++ b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
@@ -19,7 +19,7 @@ create table if not exists secd.realm_data (
create table if not exists secd.identity (
identity_id bigserial primary key
, identity_public_id uuid not null
- , data jsonb -- some things are dervied, others are not
+ , data text -- we do not prescribe JSON or any other serialization format.
, created_at timestamptz not null
, updated_at timestamptz not null
, deleted_at timestamptz
@@ -30,14 +30,18 @@ create table if not exists secd.credential (
credential_id bigserial primary key
, credential_public_id uuid not null
, identity_id bigint not null references secd.identity(identity_id)
+ , partial_key text
, type text not null-- e.g. password, oidc, totop, lookup_secret, webauthn, ...
, data jsonb not null
- , version integer not null
, created_at timestamptz not null
, revoked_at timestamptz
, deleted_at timestamptz
);
+create unique index if not exists credential_passphrase_type_key_ix
+on secd.credential (partial_key)
+where type = 'Passphrase';
+
create table if not exists secd.address (
address_id bigserial primary key
, address_public_id uuid not null
@@ -83,3 +87,23 @@ create table if not exists secd.message (
, created_at timestamptz not null
, sent_at timestamptz
);
+
+create table if not exists secd.namespace_config (
+ namespace text not null
+ , serialized_config text not null
+ , created_at xid8 not null
+ , deleted_at xid8
+ -- TODO: indexes and stuff
+);
+
+create table if not exists secd.relation_tuple (
+ namespace text not null
+ , object_id text not null
+ , relation text not null
+ , userset_namespace text not null
+ , userset_object_id text not null
+ , userset_relation text not null
+ , created_at xid8 not null
+ , deleted_at xid8 not null
+ -- TODO: indexes and stuff
+);
diff --git a/crates/secd/store/pg/sql/find_credential.sql b/crates/secd/store/pg/sql/find_credential.sql
new file mode 100644
index 0000000..e30c0ea
--- /dev/null
+++ b/crates/secd/store/pg/sql/find_credential.sql
@@ -0,0 +1,12 @@
+select c.credential_public_id
+ , i.identity_public_id
+ , c.data::text
+ , c.created_at
+ , c.revoked_at
+ , c.deleted_at
+from secd.credential c
+join secd.identity i using (identity_id)
+where (($1::uuid is null) or (c.credential_public_id = $1))
+and (($2::uuid is null) or (i.identity_public_id = $2))
+and (($3::text is null) or (c.type = $3))
+and (($3::text is null or $4::text is null) or (c.data->$3->>'key' = $4))
diff --git a/crates/secd/store/pg/sql/write_credential.sql b/crates/secd/store/pg/sql/write_credential.sql
new file mode 100644
index 0000000..17e03a2
--- /dev/null
+++ b/crates/secd/store/pg/sql/write_credential.sql
@@ -0,0 +1,19 @@
+insert into secd.credential (
+ credential_public_id
+ , identity_id
+ , partial_key
+ , type
+ , data
+ , created_at
+ , revoked_at
+ , deleted_at
+) values (
+ $1
+ , (select identity_id from secd.identity where identity_public_id = $2)
+ , $3
+ , $4
+ , $5::jsonb
+ , $6
+ , $7
+ , $8
+);
diff --git a/crates/secd/store/pg/sql/write_identity.sql b/crates/secd/store/pg/sql/write_identity.sql
index 67662a6..4b2745b 100644
--- a/crates/secd/store/pg/sql/write_identity.sql
+++ b/crates/secd/store/pg/sql/write_identity.sql
@@ -5,7 +5,7 @@ insert into secd.identity (
, updated_at
, deleted_at
) values (
- $1, $2::jsonb, $3, $4, $5
+ $1, $2, $3, $4, $5
) on conflict (identity_public_id) do update
set data = excluded.data
, updated_at = excluded.updated_at
diff --git a/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql b/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
index 299f282..b2ce45d 100644
--- a/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
+++ b/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
@@ -15,7 +15,7 @@ create table if not exists realm_data (
create table if not exists identity (
identity_id integer primary key
, identity_public_id uuid not null
- , data text -- some things are dervied, others are not
+ , data text -- we do not prescribe JSON or any other serialization format
, created_at integer not null
, updated_at integer not null
, deleted_at integer
@@ -26,14 +26,18 @@ create table if not exists credential (
credential_id integer primary key
, credential_public_id uuid not null
, identity_id integer not null references identity(identity_id)
+ , partial_key text
, type text not null-- e.g. password, oidc, totop, lookup_secret, webauthn, ...
, data text not null
- , version integer not null
, created_at integer not null
, revoked_at integer
, deleted_at integer
);
+create unique index if not exists credential_passphrase_type_key_ix
+on credential (partial_key)
+where type = 'Passphrase';
+
create table if not exists address (
address_id integer primary key
, address_public_id uuid not null
diff --git a/crates/secd/store/sqlite/sql/find_credential.sql b/crates/secd/store/sqlite/sql/find_credential.sql
new file mode 100644
index 0000000..9062914
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/find_credential.sql
@@ -0,0 +1,12 @@
+select c.credential_public_id
+ , i.identity_public_id
+ , c.data
+ , c.created_at
+ , c.revoked_at
+ , c.deleted_at
+from credential c
+join identity i using (identity_id)
+where (($1 is null) or (c.credential_public_id = $1))
+and (($2 is null) or (i.identity_public_id = $2))
+and (($3 is null) or (c.type = $3))
+and (($3 is null or $4 is null) or (c.data->$3->>'key' = $4))
diff --git a/crates/secd/store/sqlite/sql/write_credential.sql b/crates/secd/store/sqlite/sql/write_credential.sql
new file mode 100644
index 0000000..3319226
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/write_credential.sql
@@ -0,0 +1,19 @@
+insert into credential (
+ credential_public_id
+ , identity_id
+ , partial_key
+ , type
+ , data
+ , created_at
+ , revoked_at
+ , deleted_at
+) values (
+ $1
+ , (select identity_id from identity where identity_public_id = $2)
+ , $3
+ , $4
+ , $5
+ , $6
+ , $7
+ , $8
+);