aboutsummaryrefslogtreecommitdiff
path: root/crates/iam/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/iam/src/api.rs71
-rw-r--r--crates/iam/src/command.rs52
-rw-r--r--crates/iam/src/main.rs108
-rw-r--r--crates/iam/src/util.rs4
4 files changed, 175 insertions, 60 deletions
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)
+}