aboutsummaryrefslogtreecommitdiff
path: root/crates/secd/src/client
diff options
context:
space:
mode:
Diffstat (limited to '')
-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
3 files changed, 322 insertions, 74 deletions
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> {