mod client; mod command; mod util; use std::sync::Arc; use clap::ValueEnum; use client::{EmailMessenger, EmailMessengerError, Store}; use derive_more::Display; use email_address::EmailAddress; use serde::{Deserialize, Serialize}; use sqlx::FromRow; use strum_macros::{EnumString, EnumVariantNames}; use time::OffsetDateTime; use url::Url; use util::get_oauth_identity_data; 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_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, created_at: OffsetDateTime, #[serde(skip_serializing_if = "Option::is_none")] deleted_at: Option, } #[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, #[serde(with = "time::serde::timestamp")] pub created_at: OffsetDateTime, #[serde(with = "time::serde::timestamp")] pub expires_at: OffsetDateTime, #[serde(skip_serializing_if = "Option::is_none")] pub revoked_at: Option, } #[async_trait::async_trait] trait Validation { fn expired(&self) -> bool; fn is_validated(&self) -> bool; async fn find_associated_identities( &self, store: Arc, ) -> anyhow::Result>; async fn validate( &mut self, i: &Identity, store: Arc, ) -> anyhow::Result<()>; } #[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, ) -> anyhow::Result> { store.find_identity(None, Some(&self.email_address)).await } async fn validate( &mut self, i: &Identity, store: Arc, ) -> anyhow::Result<()> { self.identity_id = Some(i.id); self.validated_at = Some(OffsetDateTime::now_utc()); store.write_email_validation(&self).await?; Ok(()) } } #[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, ) -> anyhow::Result> { let oauth_identity = get_oauth_identity_data(&self).await?; let identity = store .find_identity(None, oauth_identity.email.as_deref()) .await?; 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, ) -> anyhow::Result<()> { self.identity_id = Some(i.id); self.validated_at = Some(OffsetDateTime::now_utc()); store.write_oauth_validation(&self).await?; Ok(()) } } #[derive(Debug, EnumString)] pub enum ValidationType { Email, Oauth, } #[derive(sqlx::FromRow, Debug)] pub struct EmailValidation { #[sqlx(rename = "email_validation_public_id")] id: Option, #[sqlx(rename = "identity_public_id")] identity_id: Option, #[sqlx(rename = "address")] email_address: String, code: Option, is_oauth_derived: bool, created_at: OffsetDateTime, expired_at: OffsetDateTime, validated_at: Option, revoked_at: Option, deleted_at: Option, } #[derive(Debug)] pub struct OauthValidation { id: Option, identity_id: Option, oauth_provider: OauthProvider, access_token: Option, raw_response: Option, created_at: OffsetDateTime, validated_at: Option, revoked_at: Option, deleted_at: Option, } #[derive(Debug, Clone)] pub struct OauthProvider { pub name: OauthProviderName, pub flow: Option, 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, } #[derive(Debug, Display, Clone, Copy, ValueEnum, EnumString)] pub enum OauthResponseType { Code, IdToken, None, Token, } // 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, } #[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; pub type OauthRedirectAuthUrl = Url; #[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, } pub struct Secd { store: Arc, email_messenger: Arc, } impl Secd { /// get_identity /// /// Return all information associated with the identity id. pub async fn get_identity(&self, identity: IdentityId) -> Result { 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 { 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::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!() } // 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) }