aboutsummaryrefslogtreecommitdiff
path: root/crates/secd/src/lib.rs
diff options
context:
space:
mode:
authorbenj <benj@rse8.com>2022-12-24 00:43:38 -0800
committerbenj <benj@rse8.com>2022-12-24 00:43:38 -0800
commitc2268c285648ef02ece04de0d9df0813c6d70ff8 (patch)
treef84ec7ee42f97d78245f26d0c5a0c559cd35e89d /crates/secd/src/lib.rs
parentde6339da72af1d61ca5908b780977e2b037ce014 (diff)
downloadsecdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar
secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar.gz
secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar.bz2
secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar.lz
secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar.xz
secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.tar.zst
secdiam-c2268c285648ef02ece04de0d9df0813c6d70ff8.zip
refactor everything with more abstraction and a nicer interface
Diffstat (limited to '')
-rw-r--r--crates/secd/src/lib.rs481
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>,
}