diff options
Diffstat (limited to '')
| -rw-r--r-- | crates/secd/src/lib.rs | 481 |
1 files changed, 142 insertions, 339 deletions
diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs index 17186c8..c84f7cf 100644 --- a/crates/secd/src/lib.rs +++ b/crates/secd/src/lib.rs @@ -2,382 +2,185 @@ mod client; mod command; mod util; -use std::sync::Arc; - -use clap::ValueEnum; -use client::{EmailMessenger, EmailMessengerError, Store}; -use derive_more::Display; +use client::{ + email::{EmailMessenger, EmailMessengerError}, + store::{Store, StoreError}, +}; use email_address::EmailAddress; use serde::{Deserialize, Serialize}; -use sqlx::FromRow; -use strum_macros::{EnumString, EnumVariantNames}; +use serde_with::{serde_as, DisplayFromStr}; +use std::sync::Arc; +use strum_macros::{Display, EnumString, EnumVariantNames}; use time::OffsetDateTime; use url::Url; -use util::get_oauth_identity_data; use uuid::Uuid; +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"; + 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_CODE_SIZE: usize = 6; - -const INTERNAL_ERR_MSG: &str = "It seems an invariant was borked or something non-deterministic happened. Please file a bug with secd."; - -#[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, - #[serde(skip_serializing_if = "Option::is_none")] - data: Option<String>, - created_at: OffsetDateTime, - #[serde(skip_serializing_if = "Option::is_none")] - deleted_at: Option<OffsetDateTime>, -} - -#[derive(sqlx::FromRow, Debug, Serialize)] -pub struct Session { - #[sqlx(rename = "identity_public_id")] - pub identity_id: IdentityId, - #[serde(skip_serializing_if = "Option::is_none")] - #[sqlx(default)] - pub secret: Option<SessionSecret>, - #[serde(with = "time::serde::timestamp")] - pub created_at: OffsetDateTime, - #[serde(with = "time::serde::timestamp")] - pub expired_at: OffsetDateTime, - #[serde(skip_serializing_if = "Option::is_none")] - pub revoked_at: Option<OffsetDateTime>, -} +const ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6; +const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5; +const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true; -#[async_trait::async_trait] -trait Validation { - fn expired(&self) -> bool; - fn is_validated(&self) -> bool; - async fn find_associated_identities( - &self, - store: Arc<dyn Store + Send + Sync>, - ) -> anyhow::Result<Option<Identity>>; - async fn validate( - &mut self, - i: &Identity, - store: Arc<dyn Store + Send + Sync>, - ) -> anyhow::Result<()>; -} +pub type AddressId = Uuid; +pub type AddressValidationId = Uuid; +pub type CredentialId = Uuid; +pub type IdentityId = Uuid; +pub type MotifId = Uuid; +pub type PhoneNumber = String; +pub type RefId = Uuid; +pub type SessionToken = String; -#[async_trait::async_trait] -impl Validation for EmailValidation { - fn expired(&self) -> bool { - let now = OffsetDateTime::now_utc(); - self.expired_at < now - || self.revoked_at.map(|t| t < now).unwrap_or(false) - || self.deleted_at.map(|t| t < now).unwrap_or(false) - } - fn is_validated(&self) -> bool { - self.validated_at - .map(|t| t >= OffsetDateTime::now_utc()) - .unwrap_or(false) - } - async fn find_associated_identities( - &self, - store: Arc<dyn Store + Send + Sync>, - ) -> anyhow::Result<Option<Identity>> { - store.find_identity(None, Some(&self.email_address)).await - } - async fn validate( - &mut self, - i: &Identity, - store: Arc<dyn Store + Send + Sync>, - ) -> anyhow::Result<()> { - self.identity_id = Some(i.id); - self.validated_at = Some(OffsetDateTime::now_utc()); - store.write_email_validation(&self).await?; - Ok(()) - } -} +#[derive(Debug, derive_more::Display, thiserror::Error)] +pub enum SecdError { + AddressValidationFailed, + AddressValidationSessionExchangeFailed, + AddressValidationExpiredOrConsumed, -#[async_trait::async_trait] -impl Validation for OauthValidation { - fn expired(&self) -> bool { - let now = OffsetDateTime::now_utc(); - self.revoked_at.map(|t| t < now).unwrap_or(false) - || self.deleted_at.map(|t| t < now).unwrap_or(false) - } - fn is_validated(&self) -> bool { - self.validated_at - .map(|t| t >= OffsetDateTime::now_utc()) - .unwrap_or(false) - } - async fn find_associated_identities( - &self, - store: Arc<dyn Store + Send + Sync>, - ) -> anyhow::Result<Option<Identity>> { - let oauth_identity = get_oauth_identity_data(&self).await?; + TooManyIdentities, + IdentityNotFound, - let identity = store - .find_identity(None, oauth_identity.email.as_deref()) - .await?; + EmailMessengerError(#[from] EmailMessengerError), + InvalidEmaillAddress(#[from] email_address::Error), - let now = OffsetDateTime::now_utc(); - if let Some(email) = oauth_identity.email.clone() { - let identity = identity.unwrap_or(Identity { - id: Uuid::new_v4(), - data: None, - created_at: OffsetDateTime::now_utc(), - deleted_at: None, - }); - store.write_identity(&identity).await?; - store.write_email(&email).await?; - store - .write_email_validation(&EmailValidation { - id: Some(Uuid::new_v4()), - identity_id: Some(identity.id), - email_address: email, - code: None, - is_oauth_derived: true, - created_at: now, - expired_at: now, - validated_at: Some(now), - revoked_at: None, - deleted_at: None, - }) - .await?; - Ok(Some(identity)) - } else { - Ok(identity) - } - } - async fn validate( - &mut self, - i: &Identity, - store: Arc<dyn Store + Send + Sync>, - ) -> anyhow::Result<()> { - self.identity_id = Some(i.id); - self.validated_at = Some(OffsetDateTime::now_utc()); - store.write_oauth_validation(&self).await?; - Ok(()) - } -} + FailedToProvideSessionIdentity(String), + InvalidSession, -#[derive(Debug, EnumString)] -pub enum ValidationType { - Email, - Oauth, -} - -#[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, - code: Option<String>, - is_oauth_derived: bool, - created_at: OffsetDateTime, - expired_at: OffsetDateTime, - validated_at: Option<OffsetDateTime>, - revoked_at: Option<OffsetDateTime>, - deleted_at: Option<OffsetDateTime>, -} + StoreError(#[from] StoreError), + StoreInitFailure(String), -#[derive(Debug)] -pub struct OauthValidation { - id: Option<Uuid>, - identity_id: Option<IdentityId>, - oauth_provider: OauthProvider, - access_token: Option<String>, - raw_response: Option<String>, - created_at: OffsetDateTime, - validated_at: Option<OffsetDateTime>, - revoked_at: Option<OffsetDateTime>, - deleted_at: Option<OffsetDateTime>, -} - -#[derive(Debug, Clone)] -pub struct OauthProvider { - pub name: OauthProviderName, - pub flow: Option<String>, - pub base_url: Url, - pub response: OauthResponseType, - pub default_scope: String, - pub client_id: String, - pub client_secret: String, - pub redirect_url: Url, - pub created_at: OffsetDateTime, - pub deleted_at: Option<OffsetDateTime>, -} - -#[derive(Debug, Display, Clone, Copy, ValueEnum, EnumString)] -pub enum OauthResponseType { - Code, - IdToken, - None, - Token, + FailedToDecodeInput(#[from] hex::FromHexError), + Todo, } -// TODO: feature gate ValueEnum since it's only needed for iam builds -#[derive(Copy, Display, Clone, Debug, ValueEnum, EnumString)] -pub enum OauthProviderName { - Amazon, - Apple, - Dropbox, - Facebook, - Github, - Gitlab, - Google, - Instagram, - LinkedIn, - Microsoft, - Paypal, - Reddit, - Spotify, - Strava, - Stripe, - Twitch, - Twitter, - WeChat, +pub struct Secd { + store: Arc<dyn Store + Send + Sync + 'static>, + email_messenger: Arc<dyn EmailMessenger + Send + Sync + 'static>, } #[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)] #[strum(ascii_case_insensitive)] pub enum AuthStore { - Sqlite, - Postgres, - MySql, - Mongo, - Dynamo, - Redis, + Sqlite { conn: String }, + Postgres { conn: String }, + Redis { conn: String }, } #[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)] #[strum(ascii_case_insensitive)] -pub enum AuthEmail { - LocalStub, +pub enum AuthEmailMessenger { + Local, Ses, Mailgun, Sendgrid, } -pub type IdentityId = Uuid; -pub type SessionSecret = String; -pub type SessionSecretHash = String; -pub type ValidationRequestId = Uuid; -pub type ValidationSecretCode = String; -pub type OauthRedirectAuthUrl = Url; +#[serde_with::skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct Address { + id: AddressId, + t: AddressType, + #[serde(with = "time::serde::timestamp")] + created_at: OffsetDateTime, +} -#[derive(Debug, derive_more::Display, thiserror::Error)] -pub enum SecdError { - EmailSendError(#[from] EmailMessengerError), - EmailValidationExpiryOverflow, - EmailValidationRequestError, - OauthValidationRequestError, - IdentityIdShouldExistInvariant, - InitializationFailure(sqlx::Error), - InvalidCode, - InvalidEmailAddress, - InputValidation(String), - InternalError(String), - NotImplemented(String), - SessionExpiryOverflow, - Unauthenticated, - Todo, +#[serde_as] +#[serde_with::skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct AddressValidation { + pub id: AddressValidationId, + pub identity_id: Option<IdentityId>, + pub address: Address, + pub method: AddressValidationMethod, + #[serde(with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + #[serde(with = "time::serde::timestamp")] + pub expires_at: OffsetDateTime, + #[serde(with = "time::serde::timestamp::option")] + pub revoked_at: Option<OffsetDateTime>, + #[serde(with = "time::serde::timestamp::option")] + pub validated_at: Option<OffsetDateTime>, + pub attempts: i32, + #[serde_as(as = "serde_with::hex::Hex")] + hashed_token: Vec<u8>, + #[serde_as(as = "serde_with::hex::Hex")] + hashed_code: Vec<u8>, } -pub struct Secd { - store: Arc<dyn Store + Send + Sync + 'static>, - email_messenger: Arc<dyn EmailMessenger + Send + Sync + 'static>, +#[derive(Debug, Display, Serialize, EnumString)] +pub enum AddressValidationMethod { + Email, + Sms, + Oauth, } -impl Secd { - /// 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.expired_at > OffsetDateTime::now_utc() - || session.revoked_at > Some(OffsetDateTime::now_utc()) => - { - Ok(Authorization { session }) - } - Ok(_) => Err(SecdError::Unauthenticated), - Err(_e) => Err(SecdError::Todo), - } - } - /// 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!() - } +#[derive(Debug, Display, Serialize, EnumString)] +pub enum AddressType { + Email { email_address: Option<EmailAddress> }, + Sms { phone_number: Option<PhoneNumber> }, +} - // register service - // register service_action(service_id, action) - // list services - // list service actions +#[derive(Debug, Serialize)] +pub struct Credential { + pub id: CredentialId, + pub identity_id: IdentityId, + pub t: CredentialType, +} + +#[serde_as] +#[derive(Debug, Serialize)] +pub enum CredentialType { + Passphrase { + key: String, + value: String, + }, + Oicd { + value: String, + }, + OneTimeCodes { + codes: Vec<String>, + }, + Totp { + #[serde_as(as = "DisplayFromStr")] + url: Url, + code: String, + }, + WebAuthn { + value: String, + }, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Serialize)] +pub struct Identity { + pub id: IdentityId, + pub address_validations: Vec<AddressValidation>, + pub credentials: Vec<Credential>, + pub rules: Vec<String>, // TODO: rules for (e.g. mfa reqs) + pub metadata: Option<String>, + #[serde(with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + #[serde(with = "time::serde::timestamp::option")] + pub deleted_at: Option<OffsetDateTime>, +} - // 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) +#[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>, } |
