diff options
| author | benj <benj@rse8.com> | 2022-12-01 10:30:34 -0800 |
|---|---|---|
| committer | benj <benj@rse8.com> | 2022-12-01 10:35:50 -0800 |
| commit | 2c4eb2d311919ad9fb70738199ecf99bf20c9fce (patch) | |
| tree | 8739dd9d1d0c07fc27df2ece3d21f3a03db7397b /crates/secd/src/lib.rs | |
| parent | aa8c20d501b58001a5e1b24964c62363e2112ff8 (diff) | |
| download | secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.gz secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.bz2 secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.lz secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.xz secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.zst secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.zip | |
- basic functionality with psql and sqlite
- cli helper tool
Diffstat (limited to 'crates/secd/src/lib.rs')
| -rw-r--r-- | crates/secd/src/lib.rs | 409 |
1 files changed, 409 insertions, 0 deletions
diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs new file mode 100644 index 0000000..9eb7f0e --- /dev/null +++ b/crates/secd/src/lib.rs @@ -0,0 +1,409 @@ +mod client; +mod util; + +use std::sync::Arc; + +use client::{ + email, + sqldb::{PgClient, SqliteClient}, + EmailMessenger, EmailMessengerError, Store, StoreError, +}; +use derive_more::Display; +use email_address::EmailAddress; +use log::error; +use rand::distributions::{Alphanumeric, DistString}; +use serde::{Deserialize, Serialize}; +use strum_macros::{EnumString, EnumVariantNames}; +use time::{Duration, OffsetDateTime}; +use uuid::Uuid; + +const SESSION_SIZE_BYTES: usize = 32; +const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */; +const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */; +const VALIDATION_ATTEMPTS_MAX: i32 = 5; +const VALIDATION_CODE_SIZE: usize = 6; + +#[derive(sqlx::FromRow, Debug, Serialize)] +pub struct ApiKey { + pub public_key: String, + pub private_key: String, +} + +#[derive(sqlx::FromRow, Debug, Serialize)] +pub struct Authorization { + session: Session, +} + +#[derive(sqlx::FromRow, Debug, Serialize)] +pub struct Identity { + #[sqlx(rename = "identity_public_id")] + id: Uuid, + created_at: OffsetDateTime, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option<String>, +} + +#[derive(sqlx::FromRow, Debug, Serialize)] +pub struct Session { + #[sqlx(rename = "identity_public_id")] + identity_id: IdentityId, + #[serde(skip_serializing_if = "Option::is_none")] + #[sqlx(default)] + secret: Option<SessionSecret>, + #[serde(with = "time::serde::timestamp")] + created_at: OffsetDateTime, + #[serde(with = "time::serde::timestamp")] + expires_at: OffsetDateTime, + #[serde(skip_serializing_if = "Option::is_none")] + revoked_at: Option<OffsetDateTime>, +} + +#[derive(sqlx::FromRow, Debug)] +pub struct EmailValidation { + #[sqlx(rename = "email_validation_public_id")] + id: Option<Uuid>, + #[sqlx(rename = "identity_public_id")] + identity_id: Option<IdentityId>, + #[sqlx(rename = "address")] + email_address: String, + attempts: i32, + code: String, + is_validated: bool, + created_at: OffsetDateTime, + expires_at: OffsetDateTime, + revoked_at: Option<OffsetDateTime>, +} + +#[derive(Copy, Display, Clone, Debug)] +pub enum OauthProvider { + Amazon, + Apple, + Dropbox, + Facebook, + Github, + Gitlab, + Google, + Instagram, + LinkedIn, + Microsoft, + Paypal, + Reddit, + Spotify, + Strava, + Stripe, + Twitch, + Twitter, + WeChat, +} + +#[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)] +#[strum(ascii_case_insensitive)] +pub enum AuthStore { + Sqlite, + Postgres, + MySql, + Mongo, + Dynamo, + Redis, +} + +#[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)] +#[strum(ascii_case_insensitive)] +pub enum AuthEmail { + LocalStub, + Ses, + Mailgun, + Sendgrid, +} + +pub type IdentityId = Uuid; +pub type SessionSecret = String; +pub type SessionSecretHash = String; +pub type ValidationRequestId = Uuid; +pub type ValidationSecretCode = String; + +#[derive(Debug, derive_more::Display, thiserror::Error)] +pub enum SecdError { + InvalidEmailAddress, + InvalidCode, + InitializationFailure(sqlx::Error), + IdentityIdShouldExistInvariant, + EmailSendError(#[from] EmailMessengerError), + EmailValidationRequestError, + EmailValidationExpiryOverflow, + SessionExpiryOverflow, + Unauthenticated, + Unknown, +} + +pub struct Secd { + store: Arc<dyn Store + Send + Sync + 'static>, + email_messenger: Arc<dyn EmailMessenger + Send + Sync + 'static>, +} + +impl Secd { + pub async fn init( + auth_store: AuthStore, + conn_string: Option<&str>, + email_messenger: AuthEmail, + email_template_login: Option<String>, + email_template_signup: Option<String>, + ) -> Result<Self, SecdError> { + let store = match auth_store { + AuthStore::Sqlite => { + SqliteClient::new( + sqlx::sqlite::SqlitePoolOptions::new() + .connect(conn_string.unwrap_or("sqlite::memory:".into())) + .await + .map_err(|e| SecdError::InitializationFailure(e))?, + ) + .await + } + AuthStore::Postgres => { + PgClient::new( + sqlx::postgres::PgPoolOptions::new() + .connect(conn_string.expect("No postgres connection string provided.")) + .await + .map_err(|e| SecdError::InitializationFailure(e))?, + ) + .await + } + rest @ _ => { + error!( + "requested an AuthStore which has not yet been implemented: {:?}", + rest + ); + unimplemented!() + } + }; + + let email_sender = match email_messenger { + // TODO: initialize email and SMS templates with secd + AuthEmail::LocalStub => email::LocalEmailStubber { + email_template_login, + email_template_signup, + }, + _ => unimplemented!(), + }; + + Ok(Secd { + store, + email_messenger: Arc::new(email_sender), + }) + } + /// create_validation_request + /// + /// Generate a request to validate the provided email. + pub async fn create_validation_request( + &self, + email: Option<&str>, + ) -> Result<ValidationRequestId, SecdError> { + let now = OffsetDateTime::now_utc(); + + let email = match email { + Some(ea) => { + if EmailAddress::is_valid(ea) { + ea + } else { + return Err(SecdError::InvalidEmailAddress); + } + } + None => return Err(SecdError::InvalidEmailAddress), + }; + + let mut ev = EmailValidation { + id: None, + identity_id: None, + email_address: email.to_string(), + attempts: 0, + code: Alphanumeric + .sample_string(&mut rand::thread_rng(), VALIDATION_CODE_SIZE) + .to_lowercase(), + is_validated: false, + created_at: now, + expires_at: now + .checked_add(Duration::new(EMAIL_VALIDATION_DURATION, 0)) + .ok_or(SecdError::EmailValidationExpiryOverflow)?, + revoked_at: None, + }; + + let (req_id, mail_type) = match self + .store + .find_identity(None, Some(email)) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))? + { + Some(identity) => { + let req_id = { + ev.identity_id = Some(identity.id); + self.store + .write_email_validation(&ev) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))? + }; + (req_id, client::EmailType::Login) + } + None => { + let identity = Identity { + id: Uuid::new_v4(), + created_at: OffsetDateTime::now_utc(), + data: None, + }; + self.store + .write_identity(&identity) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?; + self.store + .write_email(identity.id, email) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?; + + let req_id = { + ev.identity_id = Some(identity.id); + self.store + .write_email_validation(&ev) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))? + }; + + (req_id, client::EmailType::Signup) + } + }; + + self.email_messenger + .send_email(email, &req_id.to_string(), &ev.code, mail_type) + .await?; + + Ok(req_id) + } + /// exchange_secret_for_session + /// + /// Exchanges a secret, which consists of a validation_request_id and secret_code + /// for a session which allows authentication on behalf of the associated identity. + /// + /// Session secrets should be used to return authorization for the associated identity. + pub async fn exchange_code_for_session( + &self, + validation_request_id: ValidationRequestId, + code: ValidationSecretCode, + ) -> Result<Session, SecdError> { + let mut ev = self + .store + .find_email_validation(Some(&validation_request_id), Some(&code)) + .await + .map_err(|e| util::log_err(e.into(), SecdError::EmailValidationExpiryOverflow))?; + + if ev.is_validated + || ev.expires_at < OffsetDateTime::now_utc() + || ev.attempts >= VALIDATION_ATTEMPTS_MAX + { + return Err(SecdError::InvalidCode); + }; + + ev.is_validated = true; + ev.attempts += 1; + self.store + .write_email_validation(&ev) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?; + + // TODO: clear previous sessions if they fit the criteria + let now = OffsetDateTime::now_utc(); + let s = Session { + identity_id: ev + .identity_id + .ok_or(SecdError::IdentityIdShouldExistInvariant)?, + secret: Some(Alphanumeric.sample_string(&mut rand::thread_rng(), SESSION_SIZE_BYTES)), + created_at: now, + expires_at: now + .checked_add(Duration::new(SESSION_DURATION, 0)) + .ok_or(SecdError::SessionExpiryOverflow)?, + revoked_at: None, + }; + self.store + .write_session(&s) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?; + + Ok(s) + } + /// get_identity + /// + /// Return all information associated with the identity id. + pub async fn get_identity(&self, identity: IdentityId) -> Result<Identity, SecdError> { + unimplemented!() + } + /// get_authorization + /// + /// Return the authorization for this session. If the session is + /// invalid, expired or otherwise unauthenticated, an error will + /// be returned. + pub async fn get_authorization( + &self, + secret: SessionSecret, + ) -> Result<Authorization, SecdError> { + match self.store.read_session(&secret).await { + Ok(session) + if session.expires_at > OffsetDateTime::now_utc() + || session.revoked_at > Some(OffsetDateTime::now_utc()) => + { + Ok(Authorization { session }) + } + Ok(_) => Err(SecdError::Unauthenticated), + Err(_e) => Err(SecdError::Unknown), + } + } + /// revoke_session + /// + /// Revokes a session such that it may no longer be used to authenticate + /// the associated identity. + pub async fn revoke_session(&self, secret_hash: SessionSecretHash) -> Result<(), SecdError> { + unimplemented!() + } + /// revoke_identity + /// + /// Soft delete an identity such that all associated resources are + /// deleted as well. + /// + /// NOTE: This operation cannot be undone. Although it may not be undone + /// a separate call to delete_identity is required to cleanup necessary + /// resources. + /// + /// You may configure secd to periodically clean all revoked + /// identities and associated resources with AUTOCLEAN_REVOKED. + pub async fn revoke_identity(&self, identity_id: IdentityId) -> Result<(), SecdError> { + unimplemented!() + } + /// delete_identity + /// + /// Delete an identity and all associated resources (e.g. session, + /// authorization structures, etc...). This is a hard delete and permanently + /// removes all stored information. + /// + /// NOTE: An identity _must_ be revoked before it can be deleted. Otherwise, + /// secd will return an error. + pub async fn delete_identity(&self, identity_id: IdentityId) -> Result<(), SecdError> { + unimplemented!() + } + + // register service + // register service_action(service_id, action) + // list services + // list service actions + + // create permission + // create group (name, identities) + // create role (name, permissios) + // list group + // list role + // list permission + // describe group + // describe role + // describe permission + // add_identity_to_group + // remove_identity_from_group + // add_permission_to_role + // remove_permission_from_role + // attach_role_to_group + // attach_permission_to_group (just creates single role and attaches it) +} |
