aboutsummaryrefslogtreecommitdiff
path: root/crates/secd/src/auth
diff options
context:
space:
mode:
Diffstat (limited to 'crates/secd/src/auth')
-rw-r--r--crates/secd/src/auth/mod.rs2
-rw-r--r--crates/secd/src/auth/n.rs287
-rw-r--r--crates/secd/src/auth/z.rs54
3 files changed, 343 insertions, 0 deletions
diff --git a/crates/secd/src/auth/mod.rs b/crates/secd/src/auth/mod.rs
new file mode 100644
index 0000000..9275c79
--- /dev/null
+++ b/crates/secd/src/auth/mod.rs
@@ -0,0 +1,2 @@
+pub mod n;
+pub mod z;
diff --git a/crates/secd/src/auth/n.rs b/crates/secd/src/auth/n.rs
new file mode 100644
index 0000000..1d3b2d5
--- /dev/null
+++ b/crates/secd/src/auth/n.rs
@@ -0,0 +1,287 @@
+use std::str::FromStr;
+
+use crate::{
+ client::{
+ email::{EmailValidationMessage, Sendable},
+ store::{
+ AddressLens, AddressValidationLens, IdentityLens, SessionLens, Storable, StoreError,
+ },
+ },
+ util, Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod,
+ Credential, CredentialType, Identity, PhoneNumber, 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 time::{Duration, OffsetDateTime};
+use uuid::Uuid;
+
+impl Secd {
+ pub async fn validate_email(
+ &self,
+ email_address: &str,
+ identity_id: Option<Uuid>,
+ ) -> Result<AddressValidation, SecdError> {
+ let email_address = EmailAddress::from_str(email_address)?;
+ // record address (idempotent operation)
+ let mut address = Address {
+ id: Uuid::new_v4(),
+ t: AddressType::Email {
+ email_address: Some(email_address.clone()),
+ },
+ created_at: OffsetDateTime::now_utc(),
+ };
+
+ if let Err(StoreError::IdempotentCheckAlreadyExists) =
+ address.write(self.store.clone()).await
+ {
+ address = Address::find(
+ self.store.clone(),
+ &AddressLens {
+ id: None,
+ t: Some(&AddressType::Email {
+ email_address: Some(email_address.clone()),
+ }),
+ },
+ )
+ .await?
+ .into_iter()
+ .next()
+ .ok_or(SecdError::AddressValidationFailed)?;
+ }
+
+ let secret = hex::encode(rand::thread_rng().gen::<[u8; 32]>());
+ let code: String = vec![0; ADDRESSS_VALIDATION_CODE_SIZE as usize]
+ .into_iter()
+ .map(|_| char::from_digit(rand::thread_rng().gen_range(0..=9), 10).unwrap())
+ .collect();
+
+ let mut validation = AddressValidation {
+ id: Uuid::new_v4(),
+ identity_id,
+ address,
+ method: AddressValidationMethod::Email,
+ created_at: OffsetDateTime::now_utc(),
+ expires_at: OffsetDateTime::now_utc()
+ .checked_add(Duration::new(EMAIL_VALIDATION_DURATION, 0))
+ .ok_or(SecdError::Todo)?,
+ revoked_at: None,
+ validated_at: None,
+ attempts: 0,
+ hashed_token: util::hash(&secret.as_bytes()),
+ hashed_code: util::hash(&code.as_bytes()),
+ };
+
+ validation.write(self.store.clone()).await?;
+
+ let msg =EmailValidationMessage {
+ 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),
+ };
+
+ match msg.send().await {
+ Ok(_) => { /* TODO: Write down the message*/ }
+ Err(e) => {
+ validation.revoked_at = Some(OffsetDateTime::now_utc());
+ validation.write(self.store.clone()).await?;
+ return Err(SecdError::EmailMessengerError(e));
+ }
+ }
+
+ Ok(validation)
+ }
+ pub async fn validate_sms(
+ &self,
+ phone_number: &PhoneNumber,
+ ) -> Result<AddressValidation, SecdError> {
+ todo!()
+ }
+ pub async fn complete_address_validation(
+ &self,
+ validation_id: &AddressValidationId,
+ plaintext_token: Option<String>,
+ plaintext_code: Option<String>,
+ ) -> Result<Session, SecdError> {
+ let mut validation = AddressValidation::find(
+ self.store.clone(),
+ &AddressValidationLens {
+ id: Some(validation_id),
+ },
+ )
+ .await?
+ .into_iter()
+ .next()
+ .ok_or(SecdError::AddressValidationFailed)?;
+
+ if validation.validated_at.is_some() {
+ return Err(SecdError::AddressValidationExpiredOrConsumed);
+ }
+
+ validation.attempts += 1;
+ if validation.attempts > ADDRESS_VALIDATION_ALLOWS_ATTEMPTS as i32 {
+ warn!(
+ "validation failed: Too many validation attempts were tried for validation {:?}",
+ validation.id
+ );
+ validation.write(self.store.clone()).await?;
+ return Err(SecdError::AddressValidationExpiredOrConsumed);
+ }
+
+ let hashed_token = plaintext_token.map(|s| util::hash(s.as_bytes()));
+ let hashed_code = plaintext_code.map(|c| util::hash(c.as_bytes()));
+
+ let mut warn_msg = None;
+ match (hashed_token, hashed_code) {
+ (None, None) => {
+ warn_msg = Some("neither token nor hash was provided during the address validation session exchange");
+ }
+ (Some(t), None) => {
+ if validation.hashed_token != t {
+ warn_msg =
+ Some("the provided token does not match the address validation token");
+ }
+ }
+ (None, Some(c)) => {
+ if validation.hashed_code != c {
+ warn_msg = Some("the provided code does not match the address validation code");
+ }
+ }
+ (Some(t), Some(c)) => {
+ if validation.hashed_token != t || validation.hashed_code != c {
+ warn_msg = Some("the provided token and code must both match the address validation token and code");
+ }
+ }
+ };
+
+ if let Some(msg) = warn_msg {
+ warn!("validation failed: {}", msg);
+ validation.write(self.store.clone()).await?;
+ return Err(SecdError::AddressValidationSessionExchangeFailed);
+ }
+
+ let identity = Identity::find(
+ self.store.clone(),
+ &IdentityLens {
+ id: None,
+ address_type: Some(&validation.address.t),
+ validated_address: Some(true),
+ session_token_hash: None,
+ },
+ )
+ .await?;
+
+ if !ADDRESS_VALIDATION_IDENTITY_SURJECTION && identity.len() > 1 {
+ warn!("validation failed: identity validation surjection disallowed");
+ validation.write(self.store.clone()).await?;
+ return Err(SecdError::TooManyIdentities);
+ }
+
+ let mut identity = identity.into_iter().next();
+ if identity.is_none() {
+ let i = Identity {
+ id: Uuid::new_v4(),
+ address_validations: vec![],
+ credentials: vec![],
+ rules: vec![],
+ metadata: None,
+ created_at: OffsetDateTime::now_utc(),
+ deleted_at: None,
+ };
+ i.write(self.store.clone()).await?;
+ identity = Some(i);
+ }
+
+ assert!(identity.is_some());
+
+ // If the validation was attached to another identity, unless surjection is allowed, it cannot be recorded.
+ if !ADDRESS_VALIDATION_IDENTITY_SURJECTION
+ && validation.identity_id.is_some()
+ && identity.as_ref().map(|i| i.id) != validation.identity_id
+ {
+ warn!("validation failed: identity validation surjection is disallowed, but found existing identity for another account");
+ validation.write(self.store.clone()).await?;
+ return Err(SecdError::TooManyIdentities);
+ }
+
+ validation.identity_id = identity.map(|i| i.id);
+ 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]"))?;
+ session.write(self.store.clone()).await?;
+
+ Ok(session)
+ }
+ pub async fn create_credential(
+ &self,
+ t: CredentialType,
+ key: String,
+ value: Option<String>,
+ ) -> Result<Credential, SecdError> {
+ todo!()
+ }
+
+ pub async fn validate_credential(
+ &self,
+ t: CredentialType,
+ key: String,
+ value: Option<String>,
+ ) -> Result<Session, SecdError> {
+ todo!()
+ }
+
+ pub async fn get_session(&self, t: &SessionToken) -> Result<Session, SecdError> {
+ let token = hex::decode(t)?;
+ let mut session = Session::find(
+ self.store.clone(),
+ &SessionLens {
+ token_hash: Some(&util::hash(&token)),
+ identity_id: None,
+ },
+ )
+ .await?;
+ assert!(session.len() <= 1, "get session failed: multiple sessions found for a single token. This is very _very_ bad.");
+
+ if session.is_empty() {
+ return Err(SecdError::InvalidSession);
+ } else {
+ let mut session = session.swap_remove(0);
+ session.token = token;
+ Ok(session)
+ }
+ }
+
+ pub async fn get_identity(&self, i: &SessionToken) -> Result<Identity, SecdError> {
+ let token_hash = util::hash(&hex::decode(i)?);
+ let mut i = Identity::find(
+ self.store.clone(),
+ &IdentityLens {
+ id: None,
+ address_type: None,
+ validated_address: None,
+ session_token_hash: Some(token_hash),
+ },
+ )
+ .await?;
+
+ assert!(
+ i.len() <= 1,
+ "The provided id refers to more than one identity. This is very _very_ bad."
+ );
+
+ if i.is_empty() {
+ return Err(SecdError::IdentityNotFound);
+ } else {
+ Ok(i.swap_remove(0))
+ }
+ }
+
+ pub async fn revoke_session(&self, session: &mut Session) -> Result<(), SecdError> {
+ session.revoked_at = Some(OffsetDateTime::now_utc());
+ session.write(self.store.clone()).await?;
+ Ok(())
+ }
+}
diff --git a/crates/secd/src/auth/z.rs b/crates/secd/src/auth/z.rs
new file mode 100644
index 0000000..81c3639
--- /dev/null
+++ b/crates/secd/src/auth/z.rs
@@ -0,0 +1,54 @@
+use uuid::Uuid;
+
+use crate::{client::spice::SpiceError, Secd};
+
+#[derive(Debug, thiserror::Error, derive_more::Display)]
+pub enum AuthZError {
+ SpiceClient(#[from] SpiceError),
+ Todo,
+}
+
+pub type Namespace = String;
+pub type Object = (Namespace, Uuid);
+pub type Relation = String;
+
+pub struct Relationship {
+ pub subject: Subject,
+ pub object: Object,
+ pub relation: String,
+}
+
+#[derive(Clone)]
+pub enum Subject {
+ User(Object),
+ UserSet { user: Object, relation: Relation },
+}
+
+impl Secd {
+ pub async fn check(&self, r: &Relationship) -> Result<bool, AuthZError> {
+ let spice = self
+ .spice
+ .clone()
+ .expect("TODO: only supports postgres right now");
+
+ Ok(spice.check_permission(r).await?)
+ }
+ pub async fn expand(&self) -> Result<(), AuthZError> {
+ todo!()
+ }
+ pub async fn read(&self) -> Result<(), AuthZError> {
+ todo!()
+ }
+ pub async fn watch(&self) -> Result<(), AuthZError> {
+ unimplemented!()
+ }
+ pub async fn write(&self, ts: &[Relationship]) -> Result<(), AuthZError> {
+ let spice = self
+ .spice
+ .clone()
+ .expect("TODO: only supports postgres right now");
+
+ spice.write_relationship(ts).await?;
+ Ok(())
+ }
+}