diff options
| author | benj <benj@rse8.com> | 2022-12-12 17:06:57 -0800 |
|---|---|---|
| committer | benj <benj@rse8.com> | 2022-12-12 17:06:57 -0800 |
| commit | 0920c4d4f30a3345870d385d5c6f3e0919228b56 (patch) | |
| tree | f54668d91db469b7304758893a51b590c8f9b0de /crates/iam | |
| parent | 3a4de13528fc85dcbe6bc9055d97ba5cc87f5712 (diff) | |
| download | secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.gz secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.bz2 secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.lz secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.xz secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.zst secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.zip | |
(oauth2 + email added): a mess that may or may not really work and needs to be refactored...
Diffstat (limited to 'crates/iam')
| -rw-r--r-- | crates/iam/Cargo.toml | 5 | ||||
| -rw-r--r-- | crates/iam/src/api.rs | 71 | ||||
| -rw-r--r-- | crates/iam/src/command.rs | 52 | ||||
| -rw-r--r-- | crates/iam/src/main.rs | 108 | ||||
| -rw-r--r-- | crates/iam/src/util.rs | 4 |
5 files changed, 180 insertions, 60 deletions
diff --git a/crates/iam/Cargo.toml b/crates/iam/Cargo.toml index fd9006d..ba642c3 100644 --- a/crates/iam/Cargo.toml +++ b/crates/iam/Cargo.toml @@ -8,6 +8,7 @@ anyhow = "1.0" async-std = { version = "1.12.0", features = [ "attributes" ] } clap = { version = "4.0.29", features = ["derive"] } colored = "2.0.0" +env_logger = "0.9" home = "0.5.4" log = "0.4" rand = "0.8" @@ -16,6 +17,10 @@ serde = "1" serde_json = { version = "1.0", features = ["raw_value"] } strum = "0.24.1" strum_macros = "0.24" +tiny_http = "0.12" +tokio = { version = "1.23.0", features = ["full"] } toml = "0.5.9" thiserror = "1.0" +url = "2.3.1" +urlencoding = "2.1.2" uuid = { version = "1.2", features = ["v4", "serde"]}
\ No newline at end of file diff --git a/crates/iam/src/api.rs b/crates/iam/src/api.rs index 5b72d93..8b46d08 100644 --- a/crates/iam/src/api.rs +++ b/crates/iam/src/api.rs @@ -1,22 +1,26 @@ use crate::ISSUE_TRACKER_LOC; use clap::{Parser, Subcommand, ValueEnum}; use colored::*; +use secd::{IdentityId, OauthProviderName}; use serde::{Deserialize, Serialize}; use thiserror; +use url::Url; use uuid::Uuid; #[derive(Debug, thiserror::Error)] pub enum CliError { + #[error("{} {}", "Failed to initialize an iam store.".red(), format!("An invariant was likely broken and should be reported as a bug here: {}", ISSUE_TRACKER_LOC))] + AdminInitializationError, + #[error("{} {}", "Failed to recieve incoming request.".red(), .0.white())] + DevOauthServer(String), + #[error("{} {} {}", "An unknown error occurred.".red(), format!("An invariant was likely broken and should be reported as a bug here: {}", ISSUE_TRACKER_LOC), .0.yellow())] + InternalError(String), + #[error("{} {}", "The provided validation id and code is invalid or has expired.".red(), "You may recieve at most one session with a valid code, after which a new validation is required.")] + InvalidCode, #[error("{}", "iam failed to read a valid configuration profile. Initialize an iam store with `iam admin init`".red())] InvalidProfile, #[error("{} {}", "Failed to initialize secd: ".red(), .0.yellow())] SecdInitializationFailure(String), - #[error("{} {}", "Fail to initialize an iam store.".red(), format!("An invariant was likely broken and should be reported as a bug here: {}", ISSUE_TRACKER_LOC))] - AdminInitializationError, - #[error("{} {}", "The provided validation id and code is invalid or has expired.".red(), "You may recieve at most one session with a valid code, after which a new validation is required.")] - InvalidCode, - #[error("{} {}", "An unknown error occurred.".red(), format!("An invariant was likely broken and should be reported as a bug here: {}", ISSUE_TRACKER_LOC))] - Unknown, } #[derive(Parser)] @@ -178,11 +182,11 @@ pub enum AdminObject { public_key: Option<String>, }, /// A selected Oauth2.0 provider capable of authenticating identities - OauthProvider { - provider: OauthProvider, + Oauth2Provider { + provider: OauthProviderName, client_id: String, secret: String, - redirect_uri: String, + redirect_url: Url, }, /// A selected provider capable of sending SMS SmsProvider { @@ -320,7 +324,17 @@ pub enum CreateObject { } #[derive(Subcommand)] -pub enum DevObject {} +pub enum DevObject { + #[command( + about = "Create a temporary server to easily receive oauth validation during development.", + long_about = "Oauth2\n\nCreate a temporary server to easily receive oauth validation during development." + )] + Oauth2Server { + /// The port on which the server should listen. You must specify this exact port with your oauth provider. Defaults to 1337 + #[arg(long, short)] + port: Option<u16>, + }, +} #[derive(Subcommand)] pub enum ValidationMethod { @@ -335,9 +349,11 @@ pub enum ValidationMethod { Kerberos, /// An oauth2 provider to authenticate (and authorize) an identity Oauth2 { - provider: OauthProvider, + provider: OauthProviderName, /// An optional scope to use for authorization scope: Option<String>, + /// An optional existing identity to link to this validation request + identity: Option<IdentityId>, }, /// A phone which an identity may authenticate via SMS or voice Phone { @@ -349,28 +365,6 @@ pub enum ValidationMethod { Saml, } -#[derive(Clone, ValueEnum)] -pub enum OauthProvider { - Amazon, - Apple, - Dropbox, - Facebook, - Github, - Gitlab, - Google, - Instagram, - LinkedIn, - Microsoft, - Paypal, - Reddit, - Spotify, - Strava, - Stripe, - Twitch, - Twitter, - WeChat, -} - #[derive(Subcommand)] pub enum GetObject { ApiKey { @@ -507,3 +501,14 @@ pub struct ConfigProfile { pub email_template_login: Option<String>, pub email_template_signup: Option<String>, } + +#[derive(Serialize, Deserialize)] +pub struct Validation { + pub validation_id: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub oauth_auth_url: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub note: Option<String>, +} + +pub type ValidationSecretCode = String; diff --git a/crates/iam/src/command.rs b/crates/iam/src/command.rs index e9e0f23..980c4d0 100644 --- a/crates/iam/src/command.rs +++ b/crates/iam/src/command.rs @@ -1,6 +1,6 @@ use crate::{ - api, - util::{self, get_config_profile, Result}, + api::{self, CliError, Validation, ValidationSecretCode}, + util::{self, error_detail, get_config_profile, Result}, CONFIG_LOGIN_TEMPLATE, CONFIG_SIGNUP_TEMPLATE, }; use async_std::fs; @@ -9,10 +9,13 @@ use rand::distributions::{Alphanumeric, DistString}; use secd::{AuthEmail, AuthStore}; use std::{ fs::File, - io::{self, stdin, stdout, Write}, - str::FromStr, + io::{self, stdin, stdout, Read, Write}, + net::TcpListener, + str::{self, FromStr}, }; use strum::VariantNames; +use tiny_http::Server; +use uuid::Uuid; const DEFAULT_LOGIN_EMAIL: &str = "<!doctype html><html><body><p>You requested a login link for %secd_email_address%. Please click the following link<br/><br/>http://localhost:5500/myapp/iam/exchange/%secd_link%<br/><br/>or use code: %secd_code%</p></body></html>"; const DEFAULT_SIGNUP_EMAIL: &str = "<!doctype html><html><body><h1>Welcome to SecD IAM</h1></h1><p>If you did not request this sign up, you can safely ignore this email. Otherwise, please click the following link to validate your account<br/><br/>http://localhost:5500/myapp/iam/exchange/%secd_link%<br/><br/>or use code: %secd_code%</p></body></html>"; @@ -162,3 +165,44 @@ pub async fn admin_init(is_interactive: bool) -> Result<()> { } Ok(()) } + +pub fn dev_oauth2_listen(port: Option<u16>) -> Result<ValidationSecretCode> { + let server = Server::http(&format!("localhost:{}", port.unwrap_or(1337))).map_err(|_| { + CliError::InternalError(error_detail( + "53abd03d-c426-4bba-969d-f1dbed9af75b", + "Failure while creating a server to listen to oauth responese", + )) + })?; + + let parser = |s: &str| -> Option<ValidationSecretCode> { + let maybe_code = s.split("code=").collect::<Vec<&str>>(); + if maybe_code.len() != 2 { + None + } else { + let maybe_code = maybe_code + .last() + .map(|s| s.to_string()) + .map(|c| { + c.split("&") + .collect::<Vec<&str>>() + .first() + .map(|s| s.to_string()) + }) + .flatten(); + + maybe_code.map(|s| s.to_string()) + } + }; + + let mut s_code = String::new(); + for req in server.incoming_requests() { + match parser(req.url()) { + Some(secret_code) => { + s_code = secret_code; + break; + } + None => continue, + } + } + Ok(urlencoding::decode(&s_code)?.to_string()) +} diff --git a/crates/iam/src/main.rs b/crates/iam/src/main.rs index c187380..85b3e37 100644 --- a/crates/iam/src/main.rs +++ b/crates/iam/src/main.rs @@ -2,10 +2,17 @@ mod api; mod command; mod util; -use api::{AdminAction, Args, CliError, Command, CreateObject, GetObject, LinkObject, ListObject}; +use anyhow::bail; +use api::{ + AdminAction, AdminObject, Args, CliError, Command, CreateObject, DevObject, GetObject, + LinkObject, ListObject, Validation, +}; use clap::Parser; +use command::dev_oauth2_listen; +use env_logger::Env; use secd::{Secd, SecdError}; -use util::Result; +use util::{error_detail, Result}; +use uuid::Uuid; use crate::api::ValidationMethod; @@ -15,8 +22,9 @@ const CONFIG_LOGIN_TEMPLATE: &str = "default_login.html"; const CONFIG_SIGNUP_TEMPLATE: &str = "default_signup.html"; const ISSUE_TRACKER_LOC: &str = "https://www.github.com/secdiam/iam"; -#[async_std::main] +#[tokio::main] async fn main() { + env_logger::init_from_env(Env::default().default_filter_or("debug")); match exec().await { Ok(Some(s)) => println!("{}", s), Err(e) => { @@ -30,10 +38,15 @@ async fn main() { async fn exec() -> Result<Option<String>> { let args = Args::parse(); Ok(match args.command { - Command::Init { interactive } => admin(AdminAction::Init { interactive }) - .await - .map_err(|_| CliError::AdminInitializationError)?, - Command::Admin { action } => admin(action).await?, + Command::Init { interactive } + | Command::Admin { + action: AdminAction::Init { interactive }, + } => { + command::admin_init(interactive) + .await + .map_err(|_| CliError::AdminInitializationError)?; + None + } rest @ _ => { let cfg = util::read_config(args.profile).map_err(|_| CliError::InvalidProfile)?; @@ -48,8 +61,14 @@ async fn exec() -> Result<Option<String>> { .map_err(|e| CliError::SecdInitializationFailure(e.to_string()))?; match rest { + Command::Admin { action } => admin(&secd, action).await?, Command::Create { object } => create(&secd, object).await?, + Command::Dev { object } => dev(object).await?, Command::Get { object } => get(&secd, object).await?, + Command::Init { .. } => bail!(CliError::InternalError(error_detail( + "4a696b66-6231-4a2f-811c-4448a41473d2", + "Code path should be unreachable", + ))), Command::Link { object, unlink } => link(&secd, object, unlink).await?, Command::Ls { object, @@ -60,28 +79,30 @@ async fn exec() -> Result<Option<String>> { Command::Repl => { unimplemented!() } - _ => None, } } }) } -async fn admin(cmd: AdminAction) -> Result<Option<String>> { +async fn admin(secd: &Secd, cmd: AdminAction) -> Result<Option<String>> { Ok(match cmd { AdminAction::Backend { action } => { println!("do backend stuff!"); None } - AdminAction::Create { object } => { - println!("do create!"); - None - } - AdminAction::Init { interactive } => { - command::admin_init(interactive) - .await - .map_err(|_| CliError::AdminInitializationError)?; - None - } + AdminAction::Create { object } => match object { + AdminObject::Oauth2Provider { + provider, + client_id, + secret, + redirect_url, + } => { + secd.create_oauth_provider(&provider, client_id, secret, redirect_url) + .await?; + None + } + rest @ _ => unimplemented!(), + }, AdminAction::Seal => { println!("do seal"); None @@ -90,6 +111,9 @@ async fn admin(cmd: AdminAction) -> Result<Option<String>> { println!("do unseal: {}", secret_key); None } + AdminAction::Init { .. } => { + panic!("Invariant violation: this path should be impossible") + } }) } async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> { @@ -130,19 +154,57 @@ async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> { .await .map_err(|e| match e { SecdError::InvalidCode => CliError::InvalidCode, - _ => CliError::Unknown, + _ => CliError::InternalError(error_detail( + "17e5c226-5d7d-44a2-b3b5-be3ee958c252", + "An unknown error while exchanging a session", + )), })?; serde_json::to_string(&session).ok() } CreateObject::Validation { method, identity } => match method { - ValidationMethod::Email { address } => { - secd.create_validation_request(Some(&address)).await?; - None + ValidationMethod::Email { address } => serde_json::to_string(&Validation { + validation_id: secd.create_validation_request_email(Some(&address)).await?, + note: Some("<secret code> sent to client".into()), + oauth_auth_url: None, + }) + .ok(), + + ValidationMethod::Oauth2 { + provider, + scope, + identity, + } => { + let redirect = secd + .create_validation_request_oauth(&provider, scope) + .await? + .to_string(); + let validation_id = redirect + .split("state=") + .collect::<Vec<&str>>() + .last() + .map(|i| Uuid::parse_str(i).ok()) + .flatten() + .unwrap(); + serde_json::to_string(&Validation { + validation_id, + note: Some( + "<secret code> is retrieved by completing oauth flow in the browser".into(), + ), + oauth_auth_url: Some(redirect), + }) + .ok() } _ => unimplemented!(), }, }) } + +async fn dev(cmd: DevObject) -> Result<Option<String>> { + Ok(match cmd { + DevObject::Oauth2Server { port } => serde_json::to_string(&dev_oauth2_listen(port)?).ok(), + }) +} + async fn get(secd: &Secd, cmd: GetObject) -> Result<Option<String>> { Ok(match cmd { GetObject::ApiKey { public_key } => { diff --git a/crates/iam/src/util.rs b/crates/iam/src/util.rs index 01ce851..a74ea4a 100644 --- a/crates/iam/src/util.rs +++ b/crates/iam/src/util.rs @@ -86,3 +86,7 @@ pub fn read_config(profile_name: Option<String>) -> Result<ConfigProfile> { Ok(cfg) } + +pub fn error_detail(id: &str, d: &str) -> String { + format!("[debug info {}] {}", id, d) +} |
