From 8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3 Mon Sep 17 00:00:00 2001 From: benj Date: Fri, 30 Dec 2022 15:57:36 -0800 Subject: impl authZ write and check (depends on spicedb for now) --- crates/secd/src/auth/mod.rs | 2 + crates/secd/src/auth/n.rs | 287 ++++++++++++++++++++++++++++++++++++ crates/secd/src/auth/z.rs | 54 +++++++ crates/secd/src/client/mod.rs | 1 + crates/secd/src/client/spice/mod.rs | 154 +++++++++++++++++++ crates/secd/src/command/authn.rs | 287 ------------------------------------ crates/secd/src/command/mod.rs | 74 ---------- crates/secd/src/lib.rs | 97 +++++++++++- 8 files changed, 591 insertions(+), 365 deletions(-) create mode 100644 crates/secd/src/auth/mod.rs create mode 100644 crates/secd/src/auth/n.rs create mode 100644 crates/secd/src/auth/z.rs create mode 100644 crates/secd/src/client/spice/mod.rs delete mode 100644 crates/secd/src/command/authn.rs delete mode 100644 crates/secd/src/command/mod.rs (limited to 'crates/secd/src') 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, + ) -> Result { + 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 { + todo!() + } + pub async fn complete_address_validation( + &self, + validation_id: &AddressValidationId, + plaintext_token: Option, + plaintext_code: Option, + ) -> Result { + 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, + ) -> Result { + todo!() + } + + pub async fn validate_credential( + &self, + t: CredentialType, + key: String, + value: Option, + ) -> Result { + todo!() + } + + pub async fn get_session(&self, t: &SessionToken) -> Result { + 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 { + 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 { + 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(()) + } +} diff --git a/crates/secd/src/client/mod.rs b/crates/secd/src/client/mod.rs index e5272fd..709ecad 100644 --- a/crates/secd/src/client/mod.rs +++ b/crates/secd/src/client/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod email; +pub(crate) mod spice; pub(crate) mod store; diff --git a/crates/secd/src/client/spice/mod.rs b/crates/secd/src/client/spice/mod.rs new file mode 100644 index 0000000..d3ca30d --- /dev/null +++ b/crates/secd/src/client/spice/mod.rs @@ -0,0 +1,154 @@ +// TODO: This whole thing depends on having spice server running...which I do not want +// in a public secd library (or really at all). We will eventually get rid of this in +// favor of a light weight solution that leverages the Zanzibar API but disregards the +// scaling part. + +pub mod spice { + tonic::include_proto!("authzed.api.v1"); +} + +use spice::permissions_service_client::PermissionsServiceClient; +use spice::schema_service_client::SchemaServiceClient; +use spice::WriteSchemaRequest; +use std::env::var; +use tonic::metadata::MetadataValue; +use tonic::transport::Channel; +use tonic::{Request, Status}; + +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}; + +#[derive(Debug, thiserror::Error, derive_more::Display)] +pub enum SpiceError { + TonicTransport(#[from] tonic::transport::Error), + TonicStatus(#[from] tonic::Status), +} + +pub(crate) struct Spice { + channel: Channel, + secret: String, +} + +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"); + + let channel = Channel::from_shared(server) + .expect("invalid SPICE_SERVER uri") + .connect() + .await + .expect("initialization error: Spice failed to connect to DB."); + + Spice { channel, secret } + } + + pub async fn check_permission(&self, r: &z::Relationship) -> Result { + let mut client = + PermissionsServiceClient::with_interceptor(self.channel.clone(), |req: Request<()>| { + self.intercept(req) + }); + + let request = tonic::Request::new(CheckPermissionRequest { + consistency: Some(Consistency { + requirement: Some(consistency::Requirement::MinimizeLatency(true)), + }), + resource: Some(ObjectReference::from(&r.object)), + permission: r.relation.clone(), + subject: Some(SubjectReference::from(&r.subject)), + context: None, + }); + + let response = client.check_permission(request).await?.into_inner(); + + Ok(match Permissionship::from_i32(response.permissionship) { + Some(Permissionship::HasPermission) => true, + _ => false, + }) + } + + pub async fn write_relationship(&self, rs: &[z::Relationship]) -> Result<(), SpiceError> { + let mut client = + PermissionsServiceClient::with_interceptor(self.channel.clone(), |req: Request<()>| { + self.intercept(req) + }); + + let request = tonic::Request::new(WriteRelationshipsRequest { + updates: rs + .into_iter() + .map(|t| RelationshipUpdate { + operation: (relationship_update::Operation::Touch as i32), + relationship: Some(Relationship { + resource: Some(ObjectReference::from(&t.object)), + relation: t.relation.clone(), + subject: Some(SubjectReference::from(&t.subject)), + optional_caveat: None, + }), + }) + .collect(), + optional_preconditions: vec![], + }); + + client.write_relationships(request).await?; + + Ok(()) + } + + pub async fn write_schema(&self, schema: &str) -> Result<(), SpiceError> { + let mut client = + SchemaServiceClient::with_interceptor(self.channel.clone(), |req: Request<()>| { + self.intercept(req) + }); + let request = tonic::Request::new(WriteSchemaRequest { + schema: schema.into(), + }); + + client.write_schema(request).await?; + + Ok(()) + } + + fn intercept(&self, mut req: Request<()>) -> Result, Status> { + req.metadata_mut().insert( + "authorization", + MetadataValue::from_str(&format!("Bearer {}", self.secret)).unwrap(), + ); + Ok(req) + } +} + +impl From<&z::Subject> for SubjectReference { + fn from(s: &z::Subject) -> Self { + let tup = match s { + Subject::User(u) => (u.0.clone(), u.1.clone().to_string(), "".to_string()), + Subject::UserSet { user, relation } => { + (user.0.clone(), user.1.clone().to_string(), relation.clone()) + } + }; + + SubjectReference { + object: Some(ObjectReference { + object_type: tup.0, + object_id: tup.1, + }), + optional_relation: tup.2, + } + } +} + +impl From<&z::Object> for ObjectReference { + fn from(o: &z::Object) -> Self { + ObjectReference { + object_type: o.0.clone(), + object_id: o.1.clone().to_string(), + } + } +} diff --git a/crates/secd/src/command/authn.rs b/crates/secd/src/command/authn.rs deleted file mode 100644 index 1d3b2d5..0000000 --- a/crates/secd/src/command/authn.rs +++ /dev/null @@ -1,287 +0,0 @@ -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, - ) -> Result { - 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 { - todo!() - } - pub async fn complete_address_validation( - &self, - validation_id: &AddressValidationId, - plaintext_token: Option, - plaintext_code: Option, - ) -> Result { - 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, - ) -> Result { - todo!() - } - - pub async fn validate_credential( - &self, - t: CredentialType, - key: String, - value: Option, - ) -> Result { - todo!() - } - - pub async fn get_session(&self, t: &SessionToken) -> Result { - 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 { - 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/command/mod.rs b/crates/secd/src/command/mod.rs deleted file mode 100644 index c14cf6c..0000000 --- a/crates/secd/src/command/mod.rs +++ /dev/null @@ -1,74 +0,0 @@ -pub mod authn; - -use super::{AuthEmailMessenger, AuthStore, Secd, SecdError}; -use crate::{ - client::{ - email, - store::sql_db::{PgClient, SqliteClient}, - }, - ENV_AUTH_STORE_CONN_STRING, ENV_EMAIL_MESSENGER, ENV_EMAIL_MESSENGER_CLIENT_ID, - ENV_EMAIL_MESSENGER_CLIENT_SECRET, -}; -use log::{error, info}; -use std::{env::var, str::FromStr, sync::Arc}; - -impl Secd { - /// init - /// - /// Initialize SecD with the specified configuration, established the necessary - /// constraints, persistance stores, and options. - pub async fn init() -> Result { - let auth_store = AuthStore::from(var(ENV_AUTH_STORE_CONN_STRING).ok()); - let email_messenger = AuthEmailMessenger::from_str( - &var(ENV_EMAIL_MESSENGER).unwrap_or(AuthEmailMessenger::Local.to_string()), - ) - .expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5"); - let email_messenger_client_id = var(ENV_EMAIL_MESSENGER_CLIENT_ID).ok(); - let email_messenger_client_secret = var(ENV_EMAIL_MESSENGER_CLIENT_SECRET).ok(); - - info!("starting client with auth_store: {:?}", auth_store); - info!("starting client with email_messenger: {:?}", auth_store); - - let store = match auth_store { - AuthStore::Sqlite { conn } => { - SqliteClient::new( - sqlx::sqlite::SqlitePoolOptions::new() - .connect(&conn) - .await - .map_err(|e| { - SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e)) - })?, - ) - .await - } - AuthStore::Postgres { conn } => { - PgClient::new( - sqlx::postgres::PgPoolOptions::new() - .connect(&conn) - .await - .map_err(|e| { - SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e)) - })?, - ) - .await - } - rest @ _ => { - error!( - "requested an AuthStore which has not yet been implemented: {:?}", - rest - ); - unimplemented!() - } - }; - - let email_sender = match email_messenger { - AuthEmailMessenger::Local => email::LocalMailer {}, - _ => unimplemented!(), - }; - - Ok(Secd { - store, - email_messenger: Arc::new(email_sender), - }) - } -} diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs index c84f7cf..15a92a8 100644 --- a/crates/secd/src/lib.rs +++ b/crates/secd/src/lib.rs @@ -1,15 +1,20 @@ +pub mod auth; mod client; -mod command; mod util; use client::{ - email::{EmailMessenger, EmailMessengerError}, - store::{Store, StoreError}, + email::{EmailMessenger, EmailMessengerError, LocalMailer}, + spice::Spice, + store::{ + sql_db::{PgClient, SqliteClient}, + Store, StoreError, + }, }; use email_address::EmailAddress; +use log::{error, info}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; -use std::sync::Arc; +use std::{env::var, str::FromStr, sync::Arc}; use strum_macros::{Display, EnumString, EnumVariantNames}; use time::OffsetDateTime; use url::Url; @@ -19,6 +24,8 @@ pub const ENV_AUTH_STORE_CONN_STRING: &str = "SECD_AUTH_STORE_CONN_STRING"; 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_SPICE_SECRET: &str = "SECD_SPICE_SECRET"; +pub const ENV_SPICE_SERVER: &str = "SECD_SPICE_SERVER"; const SESSION_SIZE_BYTES: usize = 32; const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */; @@ -55,12 +62,15 @@ pub enum SecdError { StoreInitFailure(String), FailedToDecodeInput(#[from] hex::FromHexError), + + AuthorizationNotSupported(String), Todo, } pub struct Secd { store: Arc, email_messenger: Arc, + spice: Option>, } #[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)] @@ -184,3 +194,82 @@ pub struct Session { #[serde(with = "time::serde::timestamp::option")] pub revoked_at: Option, } + +impl Secd { + /// init + /// + /// Initialize SecD with the specified configuration, established the necessary + /// constraints, persistance stores, and options. + pub async fn init(z_schema: Option<&str>) -> Result { + let auth_store = AuthStore::from(var(ENV_AUTH_STORE_CONN_STRING).ok()); + let email_messenger = AuthEmailMessenger::from_str( + &var(ENV_EMAIL_MESSENGER).unwrap_or(AuthEmailMessenger::Local.to_string()), + ) + .expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5"); + let email_messenger_client_id = var(ENV_EMAIL_MESSENGER_CLIENT_ID).ok(); + let email_messenger_client_secret = var(ENV_EMAIL_MESSENGER_CLIENT_SECRET).ok(); + + info!("starting client with auth_store: {:?}", auth_store); + info!("starting client with email_messenger: {:?}", auth_store); + + let store = match auth_store { + AuthStore::Sqlite { conn } => { + if z_schema.is_some() { + return Err(SecdError::AuthorizationNotSupported( + "sqlite is currently unsupported".into(), + )); + } + + SqliteClient::new( + sqlx::sqlite::SqlitePoolOptions::new() + .connect(&conn) + .await + .map_err(|e| { + SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e)) + })?, + ) + .await + } + AuthStore::Postgres { conn } => { + PgClient::new( + sqlx::postgres::PgPoolOptions::new() + .connect(&conn) + .await + .map_err(|e| { + SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e)) + })?, + ) + .await + } + rest @ _ => { + error!( + "requested an AuthStore which has not yet been implemented: {:?}", + rest + ); + unimplemented!() + } + }; + + let email_sender = match email_messenger { + AuthEmailMessenger::Local => LocalMailer {}, + _ => unimplemented!(), + }; + + let spice = match z_schema { + Some(schema) => { + let c: Arc = Arc::new(Spice::new().await); + c.write_schema(schema) + .await + .expect("failed to write authorization schema".into()); + Some(c) + } + None => None, + }; + + Ok(Secd { + store, + email_messenger: Arc::new(email_sender), + spice, + }) + } +} -- cgit v1.2.3