pub mod auth; mod client; mod util; use async_trait::async_trait; use auth::z::{Namespace, Relation, Relationship, Subject}; use client::{ email::{EmailMessenger, EmailMessengerError, LocalMailer, Sendgrid}, spice::{Spice, SpiceError}, store::{ sql_db::{PgClient, SqliteClient}, Store, StoreError, }, }; use config::Config; use email_address::EmailAddress; use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use serde_with::{ base64::{Base64, UrlSafe}, formats::Unpadded, serde_as, }; use std::{env::var, fs::read_to_string, str::FromStr, sync::Arc}; use strum_macros::{Display, EnumString, EnumVariantNames}; use time::OffsetDateTime; use util::crypter::{Crypter, CrypterError}; use uuid::Uuid; const ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6; const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5; const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true; const API_TOKEN_SIZE_BYTES: usize = 64; const CREDENTIAL_PUBLIC_PART_BYTES: usize = 16; const CRYPTER_SECRET_KEY_DEFAULT: &str = "sup3rs3cr3t"; const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */; const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */; const SESSION_SIZE_BYTES: usize = 32; pub type AddressId = Uuid; pub type AddressValidationId = Uuid; pub type CredentialId = Uuid; pub type IdentityId = Uuid; pub type PhoneNumber = String; #[derive(Debug, derive_more::Display, thiserror::Error)] pub enum SecdError { // AuthenticationError(AuthnError); // AuthorizationError(AuthzError); // // InitializationError(...) AddressValidationFailed, AddressValidationSessionExchangeFailed, AddressValidationExpiredOrConsumed, CredentialAlreadyExists, InvalidCredential, CredentialIsNotApiToken, CrypterError(#[from] CrypterError), CfgMissingSpiceSecret, CfgMissingSpiceServer, TooManyIdentities, IdentityNotFound, IdentityAlreadyExists, ImpersonatorAlreadyExists, EmailMessengerError(#[from] EmailMessengerError), InvalidEmaillAddress(#[from] email_address::Error), FailedToProvideSessionIdentity(String), InvalidSession, ParseAssetFileError(String), StoreError(#[from] StoreError), StoreInitFailure(String), FailedToDecodeInput(#[from] hex::FromHexError), DecodeError(String), AuthorizationNotSupported(String), SpiceClient(#[from] SpiceError), Todo, } pub struct Secd { crypter: Crypter, email_messenger: Arc, spice: Option>, store: Arc, cfg: Cfg, } #[derive(Debug, Deserialize)] struct Cfg { auth_store_conn: String, crypter_secret_key: Option, email_address_from: Option, email_address_replyto: Option, email_messenger: Option, email_sendgrid_api_key: Option, email_signin_message_asset_loc: Option, email_signup_message_asset_loc: Option, email_signin_message: Option, email_signup_message: Option, spice_secret: Option, spice_server: Option, } #[async_trait] pub trait Authentication { async fn check_credential(&self, t: &CredentialType) -> Result; async fn create_credential( &self, t: &CredentialType, identity_id: Option, expires_at: Option, ) -> Result; async fn create_identity( &self, i: &Identity, t: &CredentialType, md: Option, ) -> Result; async fn impersonate( &self, impersonator: &Identity, target: &Identity, ) -> Result; async fn revoke_credential(&self, credential_id: &CredentialId) -> Result; async fn send_address_validation(&self, t: AddressType) -> Result; async fn validate_address( &self, v_id: &AddressValidationId, plaintext_token: Option, plaintext_code: Option, ) -> Result; // async fn get_identity(&self, t: &SessionToken) -> Result; } #[async_trait] pub trait Authorization { async fn check(&self, r: &Relationship) -> Result; async fn check_list_namespaces( &self, ns: &Namespace, subj: &Subject, relation: &Relation, ) -> Result, SecdError>; async fn write(&self, relationships: &[Relationship]) -> Result<(), SecdError>; } #[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)] #[strum(ascii_case_insensitive)] pub enum AuthStore { Sqlite { conn: String }, Postgres { conn: String }, Redis { conn: String }, } #[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)] #[strum(ascii_case_insensitive)] pub enum AuthEmailMessenger { Local, Ses, Mailgun, Sendgrid, } #[serde_with::skip_serializing_none] #[derive(Debug, Serialize)] pub struct Address { id: AddressId, t: AddressType, #[serde(with = "time::serde::timestamp")] created_at: OffsetDateTime, } #[serde_as] #[serde_with::skip_serializing_none] #[derive(Debug, Serialize)] pub struct AddressValidation { pub id: AddressValidationId, pub identity_id: Option, 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, #[serde(with = "time::serde::timestamp::option")] pub validated_at: Option, pub attempts: i32, #[serde_as(as = "Base64")] hashed_token: Vec, #[serde_as(as = "Base64")] hashed_code: Vec, } #[derive(Debug, Display, Serialize, EnumString)] pub enum AddressValidationMethod { Email, Sms, Oauth, } #[derive(Debug, Display, Serialize, EnumString)] pub enum AddressType { Email { email_address: Option }, Sms { phone_number: Option }, } #[serde_as] #[derive(Clone, Debug, Serialize)] pub struct Credential { pub id: CredentialId, pub identity_id: IdentityId, pub t: CredentialType, #[serde(with = "time::serde::timestamp")] pub created_at: OffsetDateTime, #[serde(with = "time::serde::timestamp::option")] pub revoked_at: Option, #[serde(with = "time::serde::timestamp::option")] pub deleted_at: Option, } #[serde_as] #[derive(Clone, Debug, Display, Serialize, Deserialize, EnumString)] pub enum CredentialType { ApiToken { public: String, private: String }, Passphrase { key: String, value: String }, Session { key: String, secret: String }, // Oidc { key: String, value: String }, // OneTimeCodes { key: String, codes: Vec }, // 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, pub credentials: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] pub new_credentials: Vec, pub rules: Vec, // TODO: rules for (e.g. mfa reqs) pub metadata: Option, #[serde(with = "time::serde::timestamp")] pub created_at: OffsetDateTime, #[serde(with = "time::serde::timestamp::option")] pub deleted_at: Option, } #[serde_with::skip_serializing_none] #[derive(Debug, Serialize)] pub struct Impersonator { pub impersonator: Identity, pub target: Identity, #[serde(with = "time::serde::timestamp")] pub created_at: OffsetDateTime, } impl Cfg { fn resolve(&mut self) -> Result<(), SecdError> { if let Some(path) = &self.email_signin_message_asset_loc { self.email_signin_message = Some(read_to_string(path).map_err(|err| { SecdError::ParseAssetFileError(format!("Email Sign In Asset [{}]: {}", path, err)) })?); } if let Some(path) = &self.email_signup_message_asset_loc { self.email_signup_message = Some(read_to_string(path).map_err(|err| { SecdError::ParseAssetFileError(format!("Email Sign In Asset [{}]: {}", path, err)) })?); } Ok(()) } } impl Secd { /// init /// /// Initialize SecD with the specified configuration, established the necessary /// constraints, persistance stores, and options. pub async fn init(cfg_path: Option<&str>, z_schema: Option<&str>) -> Result { let mut cfg: Cfg = Config::builder() .add_source(config::File::with_name(cfg_path.unwrap_or( &var("SECD_CONFIG_PATH").expect("coud not read SECD_CONFIG_PATH from environment"), ))) .build() .unwrap() .try_deserialize() .expect("failed to retrieve secd config.toml"); cfg.resolve()?; let auth_store = AuthStore::from(Some(cfg.auth_store_conn.clone())); let email_messenger = AuthEmailMessenger::from_str( &cfg.email_messenger .clone() .unwrap_or(AuthEmailMessenger::Local.to_string()), ) .expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5"); let crypter_secret_key = cfg.crypter_secret_key.clone().unwrap_or_else(|| { warn!( "NO CRYPTER KEY PROVIDED, USING DEFAULT KEY. DO NOT USE THIS KEY IN PRODUCTION. PROVIDE A UNIQUE SECRET KEY BY SETTING THE CONFIGURATION KEY. THE DEFAULT KEY IS: {}", CRYPTER_SECRET_KEY_DEFAULT, ); CRYPTER_SECRET_KEY_DEFAULT.to_string() }); info!("init with auth_store: {:?}", auth_store); info!("init with email_messenger: {:?}", email_messenger); let store = match auth_store { AuthStore::Sqlite { conn } => { if z_schema.is_some() { return Err(SecdError::AuthorizationNotSupported( "sqlite is currently unsupported".into(), )); } SqliteClient::new_ref( sqlx::sqlite::SqlitePoolOptions::new() .connect(&conn) .await .map_err(|e| { SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e)) })?, ) .await } AuthStore::Postgres { conn } => { PgClient::new_ref( sqlx::postgres::PgPoolOptions::new() .connect(&conn) .await .map_err(|e| { SecdError::StoreInitFailure(format!("failed to init postgres: {}", e)) })?, ) .await } rest => { error!( "requested an AuthStore which has not yet been implemented: {:?}", rest ); unimplemented!() } }; let email_sender = match email_messenger { AuthEmailMessenger::Local => LocalMailer::new_ref(), AuthEmailMessenger::Sendgrid => Sendgrid::new_ref( cfg.email_sendgrid_api_key .clone() .expect("No SENDGRID_API_KEY provided"), ), _ => unimplemented!(), }; let spice = match z_schema { Some(schema) => { let c: Arc = Arc::new( Spice::new( cfg.spice_secret .clone() .ok_or(SecdError::CfgMissingSpiceSecret)?, cfg.spice_server .clone() .ok_or(SecdError::CfgMissingSpiceServer)?, ) .await, ); c.write_schema(schema) .await .unwrap_or_else(|_| panic!("{}", "failed to write authorization schema")); Some(c) } None => None, }; let crypter = Crypter::new(crypter_secret_key.as_bytes()); Ok(Secd { crypter, email_messenger: email_sender, spice, store, cfg, }) } }