aboutsummaryrefslogtreecommitdiff
path: root/crates/secd
diff options
context:
space:
mode:
authorbenj <benj@rse8.com>2023-05-22 15:47:06 -0700
committerbenj <benj@rse8.com>2023-05-22 15:47:06 -0700
commited34a5251f13bbded0aa15719887db4924b351eb (patch)
tree9719d805e915f4483d5db3e5e612e8b4cf5c702c /crates/secd
parenteb92f823c31a5e702af7005231f0d6915aad3342 (diff)
downloadsecdiam-ed34a5251f13bbded0aa15719887db4924b351eb.tar
secdiam-ed34a5251f13bbded0aa15719887db4924b351eb.tar.gz
secdiam-ed34a5251f13bbded0aa15719887db4924b351eb.tar.bz2
secdiam-ed34a5251f13bbded0aa15719887db4924b351eb.tar.lz
secdiam-ed34a5251f13bbded0aa15719887db4924b351eb.tar.xz
secdiam-ed34a5251f13bbded0aa15719887db4924b351eb.tar.zst
secdiam-ed34a5251f13bbded0aa15719887db4924b351eb.zip
update credential API to include sessions
This change updates the credential API to include sessions as just another credential type. It adds the ApiToken type and enables revocation of credentials. Updates were also made to the Identity API which now includes a list of new credentials added to an Identity. This change also migrates off the hacky ENV configuration paradigm and includes a new config.toml file specified by the SECD_CONFIG_PATH env var. No default is currently provided. Clippy updates and code cleanup.
Diffstat (limited to '')
-rw-r--r--crates/secd/Cargo.toml6
-rw-r--r--crates/secd/build.rs3
-rw-r--r--crates/secd/src/auth/n.rs203
-rw-r--r--crates/secd/src/auth/z/graph.rs416
-rw-r--r--crates/secd/src/auth/z/mod.rs4
-rw-r--r--crates/secd/src/client/email/mod.rs18
-rw-r--r--crates/secd/src/client/spice/mod.rs21
-rw-r--r--crates/secd/src/client/store/mod.rs84
-rw-r--r--crates/secd/src/client/store/sql_db.rs102
-rw-r--r--crates/secd/src/lib.rs170
-rw-r--r--crates/secd/src/util/crypter.rs47
-rw-r--r--crates/secd/src/util/from.rs2
-rw-r--r--crates/secd/src/util/mod.rs275
-rw-r--r--crates/secd/store/pg/migrations/20221222002434_bootstrap.sql5
-rw-r--r--crates/secd/store/pg/sql/find_credential.sql2
-rw-r--r--crates/secd/store/pg/sql/find_identity.sql2
-rw-r--r--crates/secd/store/pg/sql/find_session.sql11
-rw-r--r--crates/secd/store/pg/sql/write_credential.sql4
-rw-r--r--crates/secd/store/pg/sql/write_identity.sql3
-rw-r--r--crates/secd/store/pg/sql/write_session.sql11
20 files changed, 768 insertions, 621 deletions
diff --git a/crates/secd/Cargo.toml b/crates/secd/Cargo.toml
index 94ae43c..2631934 100644
--- a/crates/secd/Cargo.toml
+++ b/crates/secd/Cargo.toml
@@ -6,10 +6,12 @@ edition = "2021"
[dependencies]
aes-gcm = "0.10.1"
-argon2 = "0.4.1"
+argon2 = "0.5.0"
async-trait = "0.1.59"
anyhow = "1.0"
+base64 = "0.21.0"
clap = { version = "4.0.29", features = ["derive"] }
+config = "0.13.3"
derive_more = "0.99"
email_address = "0.2"
hex = "0.4"
@@ -25,7 +27,7 @@ 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"] }
+serde_with = { version = "2.1", features = ["base64", "hex"] }
sha2 = "0.10.6"
strum = "0.24.1"
strum_macros = "0.24"
diff --git a/crates/secd/build.rs b/crates/secd/build.rs
index 8471105..e6f3362 100644
--- a/crates/secd/build.rs
+++ b/crates/secd/build.rs
@@ -8,7 +8,6 @@ fn main() {
let proto_files: Vec<PathBuf> = glob("proto/**/*.proto")
.unwrap()
- .into_iter()
.filter_map(Result::ok)
.collect();
@@ -22,7 +21,7 @@ fn main() {
tonic_build::configure()
.server_mod_attribute("attrs", "#[cfg(feature = \"server\")]")
.client_mod_attribute("attrs", "#[cfg(feature = \"client\")]")
- .file_descriptor_set_path(&descriptor_path)
+ .file_descriptor_set_path(descriptor_path)
.compile(&proto_files, &["proto"])
.unwrap();
}
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(),
diff --git a/crates/secd/src/client/email/mod.rs b/crates/secd/src/client/email/mod.rs
index 7c7b233..9e591ba 100644
--- a/crates/secd/src/client/email/mod.rs
+++ b/crates/secd/src/client/email/mod.rs
@@ -42,7 +42,7 @@ pub enum MessengerType {
pub(crate) struct LocalMailer {}
impl LocalMailer {
- pub fn new() -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
+ pub fn new_ref() -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
warn!("You are using the local mailer, which will not work in production!");
Arc::new(LocalMailer {})
}
@@ -59,7 +59,7 @@ pub(crate) struct Sendgrid {
pub api_key: String,
}
impl Sendgrid {
- pub fn new(api_key: String) -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
+ pub fn new_ref(api_key: String) -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
Arc::new(Sendgrid { api_key })
}
}
@@ -89,7 +89,7 @@ impl Sendable for EmailValidationMessage {
.subject(self.subject.clone())
.multipart(MultiPart::alternative_plain_html(
"".to_string(),
- String::from(self.body.clone()),
+ self.body.clone(),
))?;
let mailer = lettre::SmtpTransport::unencrypted_localhost();
@@ -135,7 +135,7 @@ pub(crate) fn parse_email_template(
validation_secret: Option<String>,
validation_code: Option<String>,
) -> Result<String, EmailMessengerError> {
- let mut t = template.clone().to_string();
+ let mut t = String::from(template);
// 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
@@ -143,8 +143,14 @@ pub(crate) fn parse_email_template(
// 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));
+
+ if let Some(secret) = validation_secret {
+ t = t.replace("{{secd::validation_secret}}", &secret);
+ }
+
+ if let Some(code) = validation_code {
+ t = t.replace("{{secd::validation_code}}", &code);
+ }
Ok(t)
}
diff --git a/crates/secd/src/client/spice/mod.rs b/crates/secd/src/client/spice/mod.rs
index d3ca30d..67965d7 100644
--- a/crates/secd/src/client/spice/mod.rs
+++ b/crates/secd/src/client/spice/mod.rs
@@ -3,6 +3,7 @@
// favor of a light weight solution that leverages the Zanzibar API but disregards the
// scaling part.
+#[allow(clippy::module_inception)]
pub mod spice {
tonic::include_proto!("authzed.api.v1");
}
@@ -10,7 +11,7 @@ pub mod spice {
use spice::permissions_service_client::PermissionsServiceClient;
use spice::schema_service_client::SchemaServiceClient;
use spice::WriteSchemaRequest;
-use std::env::var;
+use std::matches;
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::{Request, Status};
@@ -19,7 +20,6 @@ use crate::auth::z::{self, Subject};
use crate::client::spice::spice::{
relationship_update, ObjectReference, Relationship, RelationshipUpdate, SubjectReference,
};
-use crate::{ENV_SPICE_SECRET, ENV_SPICE_SERVER};
use self::spice::check_permission_response::Permissionship;
use self::spice::{consistency, CheckPermissionRequest, Consistency, WriteRelationshipsRequest};
@@ -36,12 +36,7 @@ pub(crate) struct Spice {
}
impl Spice {
- pub async fn new() -> Self {
- let secret =
- var(ENV_SPICE_SECRET).expect("initialization error: Failed to find SPICE_SECRET");
- let server =
- var(ENV_SPICE_SERVER).expect("initialization error: Failed to find SPICE_SERVER");
-
+ pub async fn new(secret: String, server: String) -> Self {
let channel = Channel::from_shared(server)
.expect("invalid SPICE_SERVER uri")
.connect()
@@ -69,10 +64,10 @@ impl Spice {
let response = client.check_permission(request).await?.into_inner();
- Ok(match Permissionship::from_i32(response.permissionship) {
- Some(Permissionship::HasPermission) => true,
- _ => false,
- })
+ Ok(matches!(
+ Permissionship::from_i32(response.permissionship),
+ Some(Permissionship::HasPermission)
+ ))
}
pub async fn write_relationship(&self, rs: &[z::Relationship]) -> Result<(), SpiceError> {
@@ -83,7 +78,7 @@ impl Spice {
let request = tonic::Request::new(WriteRelationshipsRequest {
updates: rs
- .into_iter()
+ .iter()
.map(|t| RelationshipUpdate {
operation: (relationship_update::Operation::Touch as i32),
relationship: Some(Relationship {
diff --git a/crates/secd/src/client/store/mod.rs b/crates/secd/src/client/store/mod.rs
index 7bf01d5..6c42dba 100644
--- a/crates/secd/src/client/store/mod.rs
+++ b/crates/secd/src/client/store/mod.rs
@@ -6,8 +6,8 @@ use std::sync::Arc;
use uuid::Uuid;
use crate::{
- util, Address, AddressType, AddressValidation, Credential, CredentialId, CredentialType,
- Identity, IdentityId, Session,
+ Address, AddressType, AddressValidation, Credential, CredentialId, CredentialType, Identity,
+ IdentityId,
};
use self::sql_db::SqlClient;
@@ -60,21 +60,13 @@ pub(crate) struct IdentityLens<'a> {
pub id: Option<&'a Uuid>,
pub address_type: Option<&'a AddressType>,
pub validated_address: Option<bool>,
- pub session_token_hash: Option<Vec<u8>>,
}
impl<'a> Lens for IdentityLens<'a> {}
-pub(crate) struct SessionLens<'a> {
- pub token_hash: Option<&'a Vec<u8>>,
- pub identity_id: Option<&'a IdentityId>,
-}
-impl<'a> Lens for SessionLens<'a> {}
-
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> {}
@@ -94,7 +86,7 @@ impl<'a> Storable<'a> for Address {
store: Arc<dyn Store>,
lens: &'a Self::Lens,
) -> Result<Vec<Self::Item>, StoreError> {
- let typ = lens.t.map(|at| at.to_string().clone());
+ let typ = lens.t.map(|at| at.to_string());
let typ = typ.as_deref();
let val = lens.t.and_then(|at| at.get_value());
@@ -151,54 +143,18 @@ impl<'a> Storable<'a> for Identity {
Ok(match store.get_type() {
StoreType::Postgres { c } => {
- c.find_identity(
- lens.id,
- val,
- lens.validated_address,
- &lens.session_token_hash,
- )
- .await?
+ c.find_identity(lens.id, val, lens.validated_address)
+ .await?
}
StoreType::Sqlite { c } => {
- c.find_identity(
- lens.id,
- val,
- lens.validated_address,
- &lens.session_token_hash,
- )
- .await?
+ c.find_identity(lens.id, val, lens.validated_address)
+ .await?
}
})
}
}
#[async_trait]
-impl<'a> Storable<'a> for Session {
- type Item = Session;
- type Lens = SessionLens<'a>;
-
- async fn write(&self, store: Arc<dyn Store>) -> Result<(), StoreError> {
- let token_hash = util::hash(&self.token);
- match store.get_type() {
- StoreType::Postgres { c } => c.write_session(self, &token_hash).await?,
- StoreType::Sqlite { c } => c.write_session(self, &token_hash).await?,
- }
- Ok(())
- }
- async fn find(
- store: Arc<dyn Store>,
- lens: &'a Self::Lens,
- ) -> Result<Vec<Self::Item>, StoreError> {
- let token = lens.token_hash.map(|t| t.clone()).unwrap_or(vec![]);
-
- Ok(match store.get_type() {
- StoreType::Postgres { c } => c.find_session(token, lens.identity_id).await?,
- StoreType::Sqlite { c } => c.find_session(token, lens.identity_id).await?,
- })
- }
-}
-
-#[async_trait]
impl<'a> Storable<'a> for Credential {
type Item = Credential;
type Lens = CredentialLens<'a>;
@@ -217,31 +173,9 @@ impl<'a> Storable<'a> for Credential {
) -> 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?
+ c.find_credential(lens.id, lens.identity_id, lens.t).await?
}
+ StoreType::Sqlite { c } => c.find_credential(lens.id, lens.identity_id, lens.t).await?,
})
}
}
diff --git a/crates/secd/src/client/store/sql_db.rs b/crates/secd/src/client/store/sql_db.rs
index 3e72fe8..7b3a68e 100644
--- a/crates/secd/src/client/store/sql_db.rs
+++ b/crates/secd/src/client/store/sql_db.rs
@@ -1,7 +1,7 @@
use super::{Store, StoreError, StoreType};
use crate::{
- Address, AddressType, AddressValidation, AddressValidationMethod, Credential, CredentialId,
- CredentialType, Identity, IdentityId, Session,
+ util::ErrorContext, Address, AddressType, AddressValidation, AddressValidationMethod,
+ Credential, CredentialId, CredentialType, Identity, IdentityId,
};
use email_address::EmailAddress;
use lazy_static::lazy_static;
@@ -26,8 +26,6 @@ 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";
-const FIND_SESSION: &str = "find_session";
const ERR_MSG_MIGRATION_FAILED: &str = "Failed to apply secd migrations to a sql db. File a bug at https://www.github.com/branchcontrol/secdiam";
@@ -59,14 +57,6 @@ lazy_static! {
include_str!("../../../store/sqlite/sql/find_identity.sql"),
),
(
- WRITE_SESSION,
- include_str!("../../../store/sqlite/sql/write_session.sql"),
- ),
- (
- FIND_SESSION,
- include_str!("../../../store/sqlite/sql/find_session.sql"),
- ),
- (
WRITE_CREDENTIAL,
include_str!("../../../store/sqlite/sql/write_credential.sql"),
),
@@ -105,14 +95,6 @@ lazy_static! {
include_str!("../../../store/pg/sql/find_identity.sql"),
),
(
- WRITE_SESSION,
- include_str!("../../../store/pg/sql/write_session.sql"),
- ),
- (
- FIND_SESSION,
- include_str!("../../../store/pg/sql/find_session.sql"),
- ),
- (
WRITE_CREDENTIAL,
include_str!("../../../store/pg/sql/write_credential.sql"),
),
@@ -145,7 +127,7 @@ impl<T> SqlxResultExt<T> for Result<T, sqlx::Error> {
return Err(StoreError::IdempotentCheckAlreadyExists);
}
}
- self.map_err(|e| StoreError::SqlClientError(e))
+ self.map_err(StoreError::SqlClientError)
}
}
@@ -169,7 +151,7 @@ impl Store for PgClient {
}
impl PgClient {
- pub async fn new(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> {
+ pub async fn new_ref(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> {
sqlx::migrate!("store/pg/migrations", "secd")
.run(&pool)
.await
@@ -196,7 +178,7 @@ impl Store for SqliteClient {
}
impl SqliteClient {
- pub async fn new(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> {
+ pub async fn new_ref(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> {
sqlx::migrate!("store/sqlite/migrations", "secd")
.run(&pool)
.await
@@ -436,7 +418,6 @@ where
id: Option<&Uuid>,
address_value: Option<&str>,
address_is_validated: Option<bool>,
- session_token_hash: &Option<Vec<u8>>,
) -> Result<Vec<Identity>, StoreError> {
let sqls = get_sqls(&self.sqls_root, FIND_IDENTITY);
let rs = sqlx::query_as::<
@@ -452,7 +433,6 @@ where
.bind(id)
.bind(address_value)
.bind(address_is_validated)
- .bind(session_token_hash)
.fetch_all(&self.pool)
.await
.extend_err()?;
@@ -462,7 +442,8 @@ where
res.push(Identity {
id,
address_validations: vec![],
- credentials: vec![],
+ credentials: self.find_credential(None, Some(id), None).await?,
+ new_credentials: vec![],
rules: vec![],
metadata,
created_at,
@@ -473,57 +454,12 @@ where
Ok(res)
}
- pub async fn write_session(&self, s: &Session, token_hash: &[u8]) -> Result<(), StoreError> {
- let sqls = get_sqls(&self.sqls_root, WRITE_SESSION);
- sqlx::query(&sqls[0])
- .bind(s.identity_id)
- .bind(token_hash)
- .bind(s.created_at)
- .bind(s.expired_at)
- .bind(s.revoked_at)
- .execute(&self.pool)
- .await
- .extend_err()?;
-
- Ok(())
- }
-
- pub async fn find_session(
- &self,
- token: Vec<u8>,
- identity_id: Option<&Uuid>,
- ) -> Result<Vec<Session>, StoreError> {
- let sqls = get_sqls(&self.sqls_root, FIND_SESSION);
- let rs =
- sqlx::query_as::<_, (Uuid, OffsetDateTime, OffsetDateTime, Option<OffsetDateTime>)>(
- &sqls[0],
- )
- .bind(token)
- .bind(identity_id)
- .bind(OffsetDateTime::now_utc())
- .bind(OffsetDateTime::now_utc())
- .fetch_all(&self.pool)
- .await
- .extend_err()?;
-
- let mut res = vec![];
- for (identity_id, created_at, expired_at, revoked_at) in rs.into_iter() {
- res.push(Session {
- identity_id,
- token: vec![],
- created_at,
- expired_at,
- revoked_at,
- });
- }
- 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,
+ CredentialType::Passphrase { key, .. } => Some(key.clone()),
+ CredentialType::ApiToken { public, .. } => Some(public.clone()),
+ CredentialType::Session { key, .. } => Some(key.clone()),
};
sqlx::query(&sqls[0])
@@ -545,17 +481,13 @@ where
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 key = t.map(|i| match i {
+ CredentialType::Passphrase { key, .. } => key.clone(),
+ CredentialType::ApiToken { public, .. } => public.clone(),
+ CredentialType::Session { key, .. } => key.clone(),
+ });
let rs = sqlx::query_as::<
_,
@@ -578,7 +510,9 @@ where
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)?;
+ let t: CredentialType =
+ serde_json::from_str(&data).ctx("error while deserializing credential_type")?;
+
res.push(Credential {
id,
identity_id,
diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs
index 54759a5..eb5d33d 100644
--- a/crates/secd/src/lib.rs
+++ b/crates/secd/src/lib.rs
@@ -12,37 +12,26 @@ use client::{
Store, StoreError,
},
};
+use config::Config;
use email_address::EmailAddress;
-use lettre::message::Mailbox;
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
-use serde_with::serde_as;
-use std::{
- env::{set_var, var},
- str::FromStr,
- sync::Arc,
+use serde_with::{
+ base64::{Base64, UrlSafe},
+ formats::Unpadded,
+ serde_as,
};
+use std::{env::var, fs::read_to_string, str::FromStr, sync::Arc};
use strum_macros::{Display, EnumString, EnumVariantNames};
use time::OffsetDateTime;
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 ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6;
const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5;
const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true;
+const API_TOKEN_SIZE_BYTES: usize = 64;
+const CREDENTIAL_PUBLIC_PART_BYTES: usize = 16;
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 */;
@@ -55,7 +44,6 @@ pub type IdentityId = Uuid;
pub type MotifId = Uuid;
pub type PhoneNumber = String;
pub type RefId = Uuid;
-pub type SessionToken = String;
#[derive(Debug, derive_more::Display, thiserror::Error)]
pub enum SecdError {
@@ -67,11 +55,14 @@ pub enum SecdError {
AddressValidationExpiredOrConsumed,
CredentialAlreadyExists,
+ InvalidCredential,
+ CredentialIsNotApiToken,
CrypterError(#[from] CrypterError),
TooManyIdentities,
IdentityNotFound,
+ IdentityAlreadyExists,
EmailMessengerError(#[from] EmailMessengerError),
InvalidEmaillAddress(#[from] email_address::Error),
@@ -79,10 +70,13 @@ pub enum SecdError {
FailedToProvideSessionIdentity(String),
InvalidSession,
+ ParseAssetFileError(String),
+
StoreError(#[from] StoreError),
StoreInitFailure(String),
FailedToDecodeInput(#[from] hex::FromHexError),
+ DecodeError(String),
AuthorizationNotSupported(String),
SpiceClient(#[from] SpiceError),
@@ -97,11 +91,20 @@ pub struct Secd {
cfg: Cfg,
}
+#[derive(Debug, Deserialize)]
struct Cfg {
- email_address_from: Option<Mailbox>,
- email_address_replyto: Option<Mailbox>,
- email_signup_message: Option<String>,
+ auth_store_conn: String,
+ crypter_secret_key: Option<String>,
+ email_address_from: Option<String>,
+ email_address_replyto: Option<String>,
+ email_messenger: Option<String>,
+ email_sendgrid_api_key: Option<String>,
+ email_signin_message_asset_loc: Option<String>,
+ email_signup_message_asset_loc: Option<String>,
email_signin_message: Option<String>,
+ email_signup_message: Option<String>,
+ spice_secret: String,
+ spice_server: String,
}
#[async_trait]
@@ -191,9 +194,9 @@ pub struct AddressValidation {
#[serde(with = "time::serde::timestamp::option")]
pub validated_at: Option<OffsetDateTime>,
pub attempts: i32,
- #[serde_as(as = "serde_with::hex::Hex")]
+ #[serde_as(as = "Base64<UrlSafe, Unpadded>")]
hashed_token: Vec<u8>,
- #[serde_as(as = "serde_with::hex::Hex")]
+ #[serde_as(as = "Base64<UrlSafe, Unpadded>")]
hashed_code: Vec<u8>,
}
@@ -225,38 +228,19 @@ pub struct Credential {
}
#[serde_as]
-#[derive(Debug, Display, Serialize, Deserialize, EnumString)]
+#[derive(Clone, Debug, Display, Serialize, Deserialize, EnumString)]
pub enum CredentialType {
+ ApiToken { public: String, private: 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> },
+ Session { key: String, secret: String },
+ // Oidc { key: String, value: String },
+ // OneTimeCodes { key: String, codes: Vec<String> },
// Totp {
// #[serde_as(as = "DisplayFromStr")]
// url: Url,
// code: String,
// },
- WebAuthn { value: String },
+ // WebAuthn { value: String },
}
#[serde_with::skip_serializing_none]
@@ -265,6 +249,8 @@ pub struct Identity {
pub id: IdentityId,
pub address_validations: Vec<AddressValidation>,
pub credentials: Vec<Credential>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ pub new_credentials: Vec<Credential>,
pub rules: Vec<String>, // TODO: rules for (e.g. mfa reqs)
pub metadata: Option<String>,
#[serde(with = "time::serde::timestamp")]
@@ -273,20 +259,22 @@ pub struct Identity {
pub deleted_at: Option<OffsetDateTime>,
}
-#[serde_as]
-#[serde_with::skip_serializing_none]
-#[derive(Debug, Serialize)]
-pub struct Session {
- pub identity_id: IdentityId,
- #[serde_as(as = "serde_with::hex::Hex")]
- #[serde(skip_serializing_if = "Vec::is_empty")]
- pub token: Vec<u8>,
- #[serde(with = "time::serde::timestamp")]
- pub created_at: OffsetDateTime,
- #[serde(with = "time::serde::timestamp")]
- pub expired_at: OffsetDateTime,
- #[serde(with = "time::serde::timestamp::option")]
- pub revoked_at: Option<OffsetDateTime>,
+impl Cfg {
+ fn resolve(&mut self) -> Result<(), SecdError> {
+ if let Some(path) = &self.email_signin_message_asset_loc {
+ self.email_signin_message = Some(read_to_string(path).map_err(|err| {
+ SecdError::ParseAssetFileError(format!("Email Sign In Asset [{}]: {}", path, err))
+ })?);
+ }
+
+ if let Some(path) = &self.email_signup_message_asset_loc {
+ self.email_signup_message = Some(read_to_string(path).map_err(|err| {
+ SecdError::ParseAssetFileError(format!("Email Sign In Asset [{}]: {}", path, err))
+ })?);
+ }
+
+ Ok(())
+ }
}
impl Secd {
@@ -294,16 +282,28 @@ impl Secd {
///
/// Initialize SecD with the specified configuration, established the necessary
/// constraints, persistance stores, and options.
- pub async fn init(z_schema: Option<&str>) -> Result<Self, SecdError> {
- let auth_store = AuthStore::from(var(ENV_AUTH_STORE_CONN_STRING).ok());
+ pub async fn init(cfg_path: Option<&str>, z_schema: Option<&str>) -> Result<Self, SecdError> {
+ let mut cfg: Cfg = Config::builder()
+ .add_source(config::File::with_name(cfg_path.unwrap_or(
+ &var("SECD_CONFIG_PATH").expect("coud not read SECD_CONFIG_PATH from environment"),
+ )))
+ .build()
+ .unwrap()
+ .try_deserialize()
+ .expect("failed to retrieve secd config.toml");
+
+ cfg.resolve()?;
+
+ let auth_store = AuthStore::from(Some(cfg.auth_store_conn.clone()));
let email_messenger = AuthEmailMessenger::from_str(
- &var(ENV_EMAIL_MESSENGER).unwrap_or(AuthEmailMessenger::Local.to_string()),
+ &cfg.email_messenger
+ .clone()
+ .unwrap_or(AuthEmailMessenger::Local.to_string()),
)
.expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5");
- let crypter_secret_key = var(ENV_CRYPTER_SECRET_KEY).unwrap_or_else(|_| {
+ let crypter_secret_key = cfg.crypter_secret_key.clone().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,
+ "NO CRYPTER KEY PROVIDED, USING DEFAULT KEY. DO NOT USE THIS KEY IN PRODUCTION. PROVIDE A UNIQUE SECRET KEY BY SETTING THE CONFIGURATION KEY. THE DEFAULT KEY IS: {}",
CRYPTER_SECRET_KEY_DEFAULT,
);
CRYPTER_SECRET_KEY_DEFAULT.to_string()
@@ -312,17 +312,6 @@ impl Secd {
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() {
@@ -331,7 +320,7 @@ impl Secd {
));
}
- SqliteClient::new(
+ SqliteClient::new_ref(
sqlx::sqlite::SqlitePoolOptions::new()
.connect(&conn)
.await
@@ -342,7 +331,7 @@ impl Secd {
.await
}
AuthStore::Postgres { conn } => {
- PgClient::new(
+ PgClient::new_ref(
sqlx::postgres::PgPoolOptions::new()
.connect(&conn)
.await
@@ -352,7 +341,7 @@ impl Secd {
)
.await
}
- rest @ _ => {
+ rest => {
error!(
"requested an AuthStore which has not yet been implemented: {:?}",
rest
@@ -362,19 +351,22 @@ impl Secd {
};
let email_sender = match email_messenger {
- AuthEmailMessenger::Local => LocalMailer::new(),
- AuthEmailMessenger::Sendgrid => Sendgrid::new(
- var(ENV_EMAIL_SENDGRID_API_KEY).expect("No SENDGRID_API_KEY provided"),
+ AuthEmailMessenger::Local => LocalMailer::new_ref(),
+ AuthEmailMessenger::Sendgrid => Sendgrid::new_ref(
+ cfg.email_sendgrid_api_key
+ .clone()
+ .expect("No SENDGRID_API_KEY provided"),
),
_ => unimplemented!(),
};
let spice = match z_schema {
Some(schema) => {
- let c: Arc<Spice> = Arc::new(Spice::new().await);
+ let c: Arc<Spice> =
+ Arc::new(Spice::new(cfg.spice_secret.clone(), cfg.spice_server.clone()).await);
c.write_schema(schema)
.await
- .expect("failed to write authorization schema".into());
+ .unwrap_or_else(|_| panic!("{}", "failed to write authorization schema"));
Some(c)
}
None => None,
diff --git a/crates/secd/src/util/crypter.rs b/crates/secd/src/util/crypter.rs
index 1717377..e5ec796 100644
--- a/crates/secd/src/util/crypter.rs
+++ b/crates/secd/src/util/crypter.rs
@@ -3,8 +3,8 @@ use aes_gcm::{
Aes256Gcm, Nonce,
};
use argon2::{
- password_hash::{self, SaltString},
- Argon2, PasswordHasher,
+ password_hash::{Salt, SaltString},
+ Argon2, PasswordHash, PasswordHasher,
};
use derive_more::Display;
use rand::Rng;
@@ -13,10 +13,10 @@ use thiserror::Error;
#[derive(Debug, Display, Error)]
pub enum CrypterError {
- EncryptError(String),
- DecryptError(String),
- DecodeError(String),
- HashError(String),
+ Encrypt(String),
+ Decrypt(String),
+ Decode(String),
+ Hash(String),
}
pub struct Crypter {
@@ -37,8 +37,8 @@ impl Crypter {
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()))?;
+ .encrypt(iv, data)
+ .map_err(|e| CrypterError::Encrypt(e.to_string()))?;
let mut msg = iv.to_vec();
msg.extend_from_slice(&crypt);
@@ -53,19 +53,36 @@ impl Crypter {
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()))?)
+ cipher
+ .decrypt(iv, data)
+ .map_err(|e| CrypterError::Decrypt(e.to_string()))
}
- pub fn hash(&self, data: &[u8]) -> Result<String, CrypterError> {
- let salt = SaltString::generate(&mut OsRng);
+ /// Hash data.
+ /// If a candidate is provided, then use that candidate's salt, passed as a full phc string.
+ pub fn hash(&self, data: &[u8], phc_candidate: Option<&str>) -> Result<String, CrypterError> {
+ let salt = match phc_candidate {
+ None => SaltString::generate(&mut OsRng).as_str().to_owned(),
+ Some(phc) => PasswordHash::new(phc)
+ .map_err(|e| CrypterError::Hash(e.to_string()))?
+ .salt.expect("unreachable fbacabb8-082a-423a-abff-06a961dc5828 [no salt found means a forced error since all resources, even those of high entropy, are salted]").as_str().to_owned(),
+ };
+
let hasher = Argon2::default();
Ok(hasher
- .hash_password(data, &salt)
- .map_err(|e| CrypterError::HashError(e.to_string()))?
+ .hash_password(
+ data,
+ Salt::from_b64(&salt).map_err(|e| CrypterError::Hash(e.to_string()))?,
+ )
+ .map_err(|e| CrypterError::Hash(e.to_string()))?
.to_string())
}
+
+ pub fn weak_hash(&self, data: &[u8]) -> Result<String, CrypterError> {
+ let mut hasher = Sha256::new();
+ hasher.update(data);
+ Ok(hex::encode(hasher.finalize()))
+ }
}
#[cfg(test)]
diff --git a/crates/secd/src/util/from.rs b/crates/secd/src/util/from.rs
index ec5b62d..7401468 100644
--- a/crates/secd/src/util/from.rs
+++ b/crates/secd/src/util/from.rs
@@ -3,7 +3,7 @@ use crate::AuthStore;
impl From<Option<String>> for AuthStore {
fn from(s: Option<String>) -> Self {
let conn = s.clone().unwrap_or("sqlite::memory:".into());
- match conn.split(":").next() {
+ match conn.split(':').next() {
Some("postgresql") | Some("postgres") => AuthStore::Postgres { conn },
Some("sqlite") => AuthStore::Sqlite { conn },
_ => panic!(
diff --git a/crates/secd/src/util/mod.rs b/crates/secd/src/util/mod.rs
index 8676f26..fb984d1 100644
--- a/crates/secd/src/util/mod.rs
+++ b/crates/secd/src/util/mod.rs
@@ -3,12 +3,14 @@ pub(crate) mod from;
use self::crypter::{Crypter, CrypterError};
use crate::{
- AddressType, Credential, CredentialType, IdentityId, SecdError, Session, SESSION_DURATION,
- SESSION_SIZE_BYTES,
+ AddressType, Credential, CredentialType, IdentityId, SecdError, API_TOKEN_SIZE_BYTES,
+ CREDENTIAL_PUBLIC_PART_BYTES, SESSION_DURATION, SESSION_SIZE_BYTES,
};
+use base64::{engine::general_purpose, Engine as _};
use sha2::{Digest, Sha256};
use std::str::from_utf8;
use time::OffsetDateTime;
+use uuid::Uuid;
pub(crate) fn hash(i: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
@@ -16,76 +18,265 @@ pub(crate) fn hash(i: &[u8]) -> Vec<u8> {
hasher.finalize().to_vec()
}
+pub trait ErrorContext<T, E: std::error::Error> {
+ fn ctx(self, err: &str) -> Result<T, E>;
+}
+
+impl<T, E: std::error::Error> ErrorContext<T, E> for Result<T, E> {
+ fn ctx(self, err: &str) -> Result<T, E> {
+ self.map_err(|e| {
+ log::error!("{} [{}]", err, e.to_string());
+ e
+ })
+ }
+}
+
impl AddressType {
pub fn get_value(&self) -> Option<String> {
match &self {
- AddressType::Email { email_address } => {
- email_address.as_ref().map(|a| a.to_string().clone())
- }
+ AddressType::Email { email_address } => email_address.as_ref().map(|a| a.to_string()),
AddressType::Sms { phone_number } => phone_number.as_ref().cloned(),
}
}
}
-impl Session {
- pub(crate) fn new(identity_id: IdentityId) -> Result<Self, SecdError> {
- let token = (0..SESSION_SIZE_BYTES)
+impl CredentialType {
+ pub fn new_api_token() -> Result<Self, SecdError> {
+ let public = general_purpose::URL_SAFE_NO_PAD.encode(
+ (0..CREDENTIAL_PUBLIC_PART_BYTES)
+ .map(|_| rand::random::<u8>())
+ .collect::<Vec<u8>>(),
+ );
+
+ let private = general_purpose::URL_SAFE_NO_PAD.encode(
+ (0..API_TOKEN_SIZE_BYTES)
+ .map(|_| rand::random::<u8>())
+ .collect::<Vec<u8>>(),
+ );
+
+ Ok(CredentialType::ApiToken { public, private })
+ }
+
+ pub fn session_from_str(token: &str) -> Result<Self, SecdError> {
+ let decoded = general_purpose::URL_SAFE_NO_PAD
+ .decode(token)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+
+ let key =
+ general_purpose::URL_SAFE_NO_PAD.encode(&decoded[0..CREDENTIAL_PUBLIC_PART_BYTES]);
+ let secret =
+ general_purpose::URL_SAFE_NO_PAD.encode(&decoded[CREDENTIAL_PUBLIC_PART_BYTES..]);
+
+ Ok(CredentialType::Session { key, secret })
+ }
+
+ pub fn api_token_from_str(token: &str) -> Result<Self, SecdError> {
+ let decoded = general_purpose::URL_SAFE_NO_PAD
+ .decode(token)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+
+ let public =
+ general_purpose::URL_SAFE_NO_PAD.encode(&decoded[0..CREDENTIAL_PUBLIC_PART_BYTES]);
+ let private =
+ general_purpose::URL_SAFE_NO_PAD.encode(&decoded[CREDENTIAL_PUBLIC_PART_BYTES..]);
+
+ Ok(CredentialType::ApiToken { public, private })
+ }
+
+ pub fn session_token(&self) -> Result<String, SecdError> {
+ match self {
+ CredentialType::Session { key, secret } => {
+ let key_bytes = general_purpose::URL_SAFE_NO_PAD
+ .decode(key)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+ let secret_bytes = general_purpose::URL_SAFE_NO_PAD
+ .decode(secret)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+
+ let mut input = key_bytes;
+ input.extend(secret_bytes);
+
+ Ok(general_purpose::URL_SAFE_NO_PAD.encode(input))
+ }
+ _ => Err(SecdError::InvalidCredential),
+ }
+ .ctx("the credential type is not a session")
+ }
+
+ pub fn api_token(&self) -> Result<String, SecdError> {
+ match self {
+ CredentialType::ApiToken { public, private } => {
+ let public_bytes = general_purpose::URL_SAFE_NO_PAD
+ .decode(public)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+ let private_bytes = general_purpose::URL_SAFE_NO_PAD
+ .decode(private)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+
+ let mut input = public_bytes;
+ input.extend(private_bytes);
+
+ Ok(general_purpose::URL_SAFE_NO_PAD.encode(input))
+ }
+ _ => Err(SecdError::InvalidCredential),
+ }
+ .ctx("the credential type is not an api token")
+ }
+}
+
+impl Credential {
+ pub fn new_session(identity_id: IdentityId) -> Result<Self, SecdError> {
+ let key = (0..CREDENTIAL_PUBLIC_PART_BYTES)
+ .map(|_| rand::random::<u8>())
+ .collect::<Vec<u8>>();
+
+ let secret = (0..SESSION_SIZE_BYTES)
.map(|_| rand::random::<u8>())
.collect::<Vec<u8>>();
+
let now = OffsetDateTime::now_utc();
- Ok(Session {
+ Ok(Credential {
+ id: Uuid::new_v4(),
identity_id,
- token,
+ t: CredentialType::Session {
+ key: general_purpose::URL_SAFE_NO_PAD.encode(key),
+ secret: general_purpose::URL_SAFE_NO_PAD.encode(secret),
+ },
created_at: now,
- expired_at: now
- .checked_add(time::Duration::new(SESSION_DURATION, 0))
- .ok_or(SecdError::Todo)?,
- revoked_at: None,
+ revoked_at: Some(
+ now.checked_add(time::Duration::new(SESSION_DURATION, 0))
+ .ok_or(SecdError::Todo)?,
+ ),
+ deleted_at: None,
})
}
-}
-impl Credential {
- pub(crate) fn encrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
- Ok(match self.t {
- CredentialType::Passphrase {
- key: _,
- ref mut value,
+ pub fn encrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ match self.t {
+ CredentialType::ApiToken {
+ ref mut private, ..
} => {
+ *private = hex::encode(crypter.encrypt(private.as_bytes())?);
+ }
+ CredentialType::Passphrase { ref mut value, .. } => {
*value = hex::encode(crypter.encrypt(value.as_bytes())?);
}
- _ => {}
- })
+ CredentialType::Session { ref mut secret, .. } => {
+ *secret = hex::encode(crypter.encrypt(secret.as_bytes())?);
+ }
+ };
+ Ok(())
}
- pub(crate) fn decrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
- Ok(match self.t {
- CredentialType::Passphrase {
- key: _,
- ref mut value,
+ pub fn decrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ match self.t {
+ CredentialType::ApiToken {
+ ref mut private, ..
} => {
- *value = from_utf8(
+ *private = from_utf8(
&crypter.decrypt(
- &hex::decode(value.clone())
- .map_err(|e| CrypterError::DecodeError(e.to_string()))?,
+ &hex::decode(private.clone())
+ .map_err(|e| CrypterError::Decode(e.to_string()))?,
)?,
)
- .map_err(|e| CrypterError::DecodeError(e.to_string()))?
+ .map_err(|e| CrypterError::Decode(e.to_string()))?
.to_string()
}
- _ => {}
- })
+ CredentialType::Passphrase { ref mut value, .. } => {
+ *value = from_utf8(&crypter.decrypt(
+ &hex::decode(value.clone()).map_err(|e| CrypterError::Decode(e.to_string()))?,
+ )?)
+ .map_err(|e| CrypterError::Decode(e.to_string()))?
+ .to_string()
+ }
+ CredentialType::Session { ref mut secret, .. } => {
+ *secret = from_utf8(
+ &crypter.decrypt(
+ &hex::decode(secret.clone())
+ .map_err(|e| CrypterError::Decode(e.to_string()))?,
+ )?,
+ )
+ .map_err(|e| CrypterError::Decode(e.to_string()))?
+ .to_string()
+ }
+ };
+ Ok(())
}
- pub(crate) fn hash(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
- Ok(match self.t {
- CredentialType::Passphrase {
- key: _,
- ref mut value,
+ pub fn hash(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ match self.t {
+ CredentialType::ApiToken {
+ ref mut private, ..
} => {
- *value = crypter.hash(value.as_bytes())?;
+ *private = crypter.weak_hash(private.as_bytes())?;
}
- _ => {}
- })
+ CredentialType::Passphrase { ref mut value, .. } => {
+ *value = crypter.hash(value.as_bytes(), None)?;
+ }
+ CredentialType::Session { ref mut secret, .. } => {
+ *secret = crypter.weak_hash(secret.as_bytes())?;
+ }
+ };
+ Ok(())
+ }
+
+ pub fn hash_compare(
+ &mut self,
+ plaintext: &CredentialType,
+ crypter: &Crypter,
+ ) -> Result<(), SecdError> {
+ match (&self.t, plaintext) {
+ (
+ CredentialType::ApiToken {
+ public: current_public,
+ private: current_private,
+ },
+ CredentialType::ApiToken {
+ public: plaintext_public,
+ private: plaintext_private,
+ },
+ ) => {
+ let plaintext_hash = crypter.weak_hash(plaintext_private.as_bytes())?;
+ if plaintext_public != current_public || &plaintext_hash != current_private {
+ return Err(SecdError::InvalidCredential);
+ }
+ }
+ (
+ CredentialType::Passphrase {
+ key: current_key,
+ value: current_value,
+ },
+ CredentialType::Passphrase {
+ key: plaintext_key,
+ value: plaintext_value,
+ },
+ ) => {
+ let plaintext_hash =
+ crypter.hash(plaintext_value.as_bytes(), Some(current_value))?;
+ if plaintext_key != current_key || &plaintext_hash != current_value {
+ return Err(SecdError::InvalidCredential);
+ }
+ }
+ (
+ CredentialType::Session {
+ key: current_key,
+ secret: current_secret,
+ },
+ CredentialType::Session {
+ key: plaintext_key,
+ secret: plaintext_secret,
+ },
+ ) => {
+ let plaintext_hash = crypter.weak_hash(plaintext_secret.as_bytes())?;
+ if plaintext_key != current_key || &plaintext_hash != current_secret {
+ return Err(SecdError::InvalidCredential);
+ }
+ }
+ _ => panic!(
+ "unreachable 78cfff7c-5493-42c5-add7-044241b3d713 [different credential types]"
+ ),
+ }
+
+ Ok(())
}
}
diff --git a/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
index 0cf3fa0..0fd423e 100644
--- a/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
+++ b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
@@ -36,11 +36,10 @@ create table if not exists secd.credential (
, created_at timestamptz not null
, revoked_at timestamptz
, deleted_at timestamptz
+ , unique(partial_key)
);
-create unique index if not exists credential_passphrase_type_key_ix
-on secd.credential (partial_key)
-where type = 'Passphrase';
+create unique index if not exists credential_partial_key_type_key_ix on secd.credential (partial_key);
create table if not exists secd.address (
address_id bigserial primary key
diff --git a/crates/secd/store/pg/sql/find_credential.sql b/crates/secd/store/pg/sql/find_credential.sql
index e30c0ea..1736500 100644
--- a/crates/secd/store/pg/sql/find_credential.sql
+++ b/crates/secd/store/pg/sql/find_credential.sql
@@ -9,4 +9,4 @@ 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))
+and (($3::text is null or $4::text is null) or (c.partial_key = $4))
diff --git a/crates/secd/store/pg/sql/find_identity.sql b/crates/secd/store/pg/sql/find_identity.sql
index 37105cb..41c8518 100644
--- a/crates/secd/store/pg/sql/find_identity.sql
+++ b/crates/secd/store/pg/sql/find_identity.sql
@@ -7,9 +7,7 @@ select distinct
from secd.identity i
left join secd.address_validation av using (identity_id)
left join secd.address a using (address_id)
-left join secd.session s using (identity_id)
where (($1::uuid is null) or (i.identity_public_id = $1))
and (($2::text is null) or (a.value = $2))
and (($3::bool is null) or (($3::bool is true) and (av.validated_at is not null)))
-and (($4::bytea is null) or (s.token_hash = $4))
and i.deleted_at is null;
diff --git a/crates/secd/store/pg/sql/find_session.sql b/crates/secd/store/pg/sql/find_session.sql
deleted file mode 100644
index ca58480..0000000
--- a/crates/secd/store/pg/sql/find_session.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-select distinct
- i.identity_public_id
- , s.created_at
- , s.expired_at
- , s.revoked_at
-from secd.session s
-join secd.identity i using (identity_id)
-where (($1::bytea is null) or (s.token_hash = $1))
-and (($2::uuid is null) or (i.identity_public_id = $2))
-and (($3::timestamptz is null) or (s.expired_at > $3))
-and ((revoked_at is null) or ($4::timestamptz is null) or (s.revoked_at > $4));
diff --git a/crates/secd/store/pg/sql/write_credential.sql b/crates/secd/store/pg/sql/write_credential.sql
index 17e03a2..ecaf523 100644
--- a/crates/secd/store/pg/sql/write_credential.sql
+++ b/crates/secd/store/pg/sql/write_credential.sql
@@ -16,4 +16,6 @@ insert into secd.credential (
, $6
, $7
, $8
-);
+) on conflict (partial_key) do update
+ set revoked_at = excluded.revoked_at
+ , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/pg/sql/write_identity.sql b/crates/secd/store/pg/sql/write_identity.sql
index 4b2745b..e86d2f5 100644
--- a/crates/secd/store/pg/sql/write_identity.sql
+++ b/crates/secd/store/pg/sql/write_identity.sql
@@ -9,4 +9,5 @@ insert into secd.identity (
) on conflict (identity_public_id) do update
set data = excluded.data
, updated_at = excluded.updated_at
- , deleted_at = excluded.deleted_at;
+ , deleted_at = excluded.deleted_at
+returning (xmax = 0);
diff --git a/crates/secd/store/pg/sql/write_session.sql b/crates/secd/store/pg/sql/write_session.sql
deleted file mode 100644
index aa9c0a1..0000000
--- a/crates/secd/store/pg/sql/write_session.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-insert into secd.session (
- identity_id
- , token_hash
- , created_at
- , expired_at
- , revoked_at
-) values (
- (select identity_id from secd.identity where identity_public_id = $1)
- , $2, $3, $4, $5
-) on conflict (token_hash) do update
- set revoked_at = excluded.revoked_at;