From 945ea27231eb34b16a1275630eb9b67bda5bc96e Mon Sep 17 00:00:00 2001 From: Jared Beller Date: Tue, 7 Mar 2023 00:56:10 -0500 Subject: [PATCH] switch to environment variables for config --- .env.example | 7 ++ .gitignore | 2 +- Cargo.lock | 249 +++---------------------------------- Cargo.toml | 3 +- config/config_example.toml | 7 -- src/config.rs | 160 +++++++++++++++++++----- src/main.rs | 60 +++------ 7 files changed, 168 insertions(+), 320 deletions(-) create mode 100644 .env.example delete mode 100644 config/config_example.toml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1015f57 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +POSER_AUTH_COOKIE_SECRET="" + +POSER_AUTH_GOOGLE_CLIENT_ID="" +POSER_AUTH_GOOGLE_CLIENT_SECRET="" +POSER_AUTH_GOOGLE_ALLOWED_DOMAINS="" +POSER_AUTH_GOOGLE_AUTHORIZE_URL="https://accounts.google.com/o/oauth2/v2/auth?hd=" +POSER_AUTH_GOOGLE_ADMIN_EMAIL="admin@domain" diff --git a/.gitignore b/.gitignore index 04efa55..37201ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ result -config/config.toml +.env target/ diff --git a/Cargo.lock b/Cargo.lock index 6d8afd1..51e9a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,55 +121,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" - [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "clap" -version = "4.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3" -dependencies = [ - "bitflags", - "clap_derive", - "clap_lex", - "is-terminal", - "once_cell", - "strsim", - "termcolor", -] - -[[package]] -name = "clap_derive" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" -dependencies = [ - "os_str_bytes", -] - [[package]] name = "cookie" version = "0.17.0" @@ -196,27 +153,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "fiat-crypto" version = "0.1.17" @@ -297,12 +233,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "heck" version = "0.4.1" @@ -318,12 +248,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - [[package]] name = "http" version = "0.2.9" @@ -387,38 +311,6 @@ dependencies = [ "want", ] -[[package]] -name = "indexmap" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" -dependencies = [ - "libc", - "windows-sys 0.45.0", -] - -[[package]] -name = "is-terminal" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.45.0", -] - [[package]] name = "itoa" version = "1.0.5" @@ -446,12 +338,6 @@ version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" -[[package]] -name = "linux-raw-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" - [[package]] name = "lock_api" version = "0.4.9" @@ -510,15 +396,6 @@ dependencies = [ "windows-sys 0.45.0", ] -[[package]] -name = "nom8" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" -dependencies = [ - "memchr", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -535,7 +412,7 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] @@ -556,12 +433,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - [[package]] name = "overload" version = "0.1.1" @@ -653,12 +524,11 @@ dependencies = [ "anyhow", "axum", "base64ct", - "clap", "pasetors", "serde", "serde_json", + "thiserror", "tokio", - "toml", "tower", "tower-cookies", "tower-http", @@ -666,30 +536,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.51" @@ -743,20 +589,6 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" -[[package]] -name = "rustix" -version = "0.36.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys 0.45.0", -] - [[package]] name = "rustversion" version = "1.0.11" @@ -815,15 +647,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -879,12 +702,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "subtle" version = "2.4.1" @@ -909,12 +726,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] -name = "termcolor" -version = "1.2.0" +name = "thiserror" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" dependencies = [ - "winapi-util", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -985,40 +813,6 @@ dependencies = [ "syn", ] -[[package]] -name = "toml" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5" -dependencies = [ - "indexmap", - "nom8", - "serde", - "serde_spanned", - "toml_datetime", -] - [[package]] name = "tower" version = "0.4.13" @@ -1257,15 +1051,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 337cb38..348200d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,12 +7,11 @@ edition = "2021" anyhow = "1.0" axum = { version = "0.6", features = ["macros"] } base64ct = { version = "1.5", features = ["alloc"] } -clap = { version = "4.1", features = ["derive"] } pasetors = "0.6" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +thiserror = "1.0" tokio = { version = "1.25", features = ["full"] } -toml = "0.7" tower = "0.4" tower-cookies = "0.9" tower-http = { version = "0.3", features = ["trace"] } diff --git a/config/config_example.toml b/config/config_example.toml deleted file mode 100644 index 3803d33..0000000 --- a/config/config_example.toml +++ /dev/null @@ -1,7 +0,0 @@ -[google] -client_id = "" -client_secret = "" -allowed_domains = [""] -authorize_url = "https://accounts.google.com/o/oauth2/v2/auth?hd=" -service_account_file = "/data/config/service_account.json" -admin_email = "" diff --git a/src/config.rs b/src/config.rs index c0ef1f3..acb1a5b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,69 +1,161 @@ -use std::net::{IpAddr, Ipv6Addr}; +use std::env::{var, VarError}; +use std::net::SocketAddr; use std::path::PathBuf; use base64ct::{Base64, Encoding}; use pasetors::{keys::SymmetricKey, version4::V4}; -use serde::{Deserialize, Deserializer}; +use thiserror::Error; +use tracing::error; -const DEFAULT_ADDR: IpAddr = IpAddr::V6(Ipv6Addr::LOCALHOST); -const DEFAULT_PORT: u16 = 8080; +#[derive(Error, Clone, Debug)] +pub enum ConfigError { + #[error("invalid environment variable")] + InvalidEnvVar, + #[error("invalid socket address")] + InvalidAddr, + #[error("invalid base64")] + InvalidBase64, + #[error("invalid symmetric key")] + InvalidSymmetricKey, + #[error("missing cookie secret")] + MissingCookieSecret, + #[error("missing Google client id")] + MissingClientId, + #[error("missing Google client secret")] + MissingClientSecret, + #[error("missing Google admin email")] + MissingAdminEmail, +} + +// Environment variables for each config option +const ENV_LISTEN_ADDR: &str = "POSER_AUTH_LISTEN_ADDR"; + +const ENV_COOKIE_NAME: &str = "POSER_AUTH_COOKIE_NAME"; +const ENV_COOKIE_SECRET: &str = "POSER_AUTH_COOKIE_SECRET"; + +const ENV_GOOGLE_CLIENT_ID: &str = "POSER_AUTH_GOOGLE_CLIENT_ID"; +const ENV_GOOGLE_CLIENT_SECRET: &str = "POSER_AUTH_GOOGLE_CLIENT_SECRET"; +const ENV_GOOGLE_ALLOWED_DOMAINS: &str = "POSER_AUTH_GOOGLE_ALLOWED_DOMAINS"; +const ENV_GOOGLE_AUTH_URL: &str = "POSER_AUTH_GOOGLE_AUTH_URL"; +const ENV_GOOGLE_SERVICE_ACCOUNT: &str = "POSER_AUTH_GOOGLE_SERVICE_ACCOUNT"; +const ENV_GOOGLE_ADMIN_EMAIL: &str = "POSER_AUTH_GOOGLE_ADMIN_EMAIL"; + +// Default values for some config options +const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:8080"; const DEFAULT_COOKIE_NAME: &str = "_poser_auth"; -const DEFAULT_AUTH_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth"; +const DEFAULT_GOOGLE_AUTH_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth"; +const DEFAULT_GOOGLE_SERVICE_ACCOUNT: &str = "/data/service_account.json"; -#[derive(Deserialize, Clone, Debug)] +#[derive(Clone, Debug)] pub struct Config { - #[serde(default = "default_addr")] - pub ip: IpAddr, - #[serde(default = "default_port")] - pub port: u16, + pub addr: SocketAddr, pub cookie: CookieConfig, pub google: GoogleConfig, } -#[derive(Deserialize, Clone, Debug)] +#[derive(Clone, Debug)] pub struct CookieConfig { - #[serde(default = "default_cookie_name")] pub name: String, - #[serde(deserialize_with = "from_base64")] pub secret: SymmetricKey, } -#[derive(Deserialize, Clone, Debug)] +#[derive(Clone, Debug)] pub struct GoogleConfig { pub client_id: String, pub client_secret: String, pub allowed_domains: Vec, - #[serde(default = "default_auth_url")] pub auth_url: String, pub service_account_file: PathBuf, pub admin_email: String, } -fn from_base64<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - use serde::de::Error; - - String::deserialize(deserializer) - .and_then(|s| Base64::decode_vec(&s).map_err(|e| Error::custom(e.to_string()))) - .and_then(|b| SymmetricKey::::from(&b).map_err(|e| Error::custom(e.to_string()))) +impl Config { + pub fn try_env() -> Result { + parse_config() + } } -const fn default_addr() -> IpAddr { - DEFAULT_ADDR -} +pub fn parse_config() -> Result { + let addr = get_env(ENV_LISTEN_ADDR)? + .unwrap_or_else(|| DEFAULT_LISTEN_ADDR.to_string()) + .parse() + .map_err(|_| { + error!("could not parse socket address"); + ConfigError::InvalidAddr + })?; -const fn default_port() -> u16 { - DEFAULT_PORT -} + let cookie = { + let name = get_env(ENV_COOKIE_NAME)?.unwrap_or_else(|| DEFAULT_COOKIE_NAME.to_string()); + + let secret_encoded = get_env(ENV_COOKIE_SECRET)?.ok_or_else(|| { + error!("expected cookie secret"); + ConfigError::MissingCookieSecret + })?; + let secret_decoded = Base64::decode_vec(&secret_encoded).map_err(|_| { + error!("failed to decode cookie secret as base64"); + ConfigError::InvalidBase64 + })?; + let secret = SymmetricKey::::from(&secret_decoded).map_err(|_| { + error!("failed to interprete cookie secret as encryption key"); + ConfigError::InvalidSymmetricKey + })?; + + CookieConfig { name, secret } + }; + + let google = { + let client_id = get_env(ENV_GOOGLE_CLIENT_ID)?.ok_or_else(|| { + error!("expected Google client id"); + ConfigError::MissingClientId + })?; + + let client_secret = get_env(ENV_GOOGLE_CLIENT_SECRET)?.ok_or_else(|| { + error!("expected Google client secret"); + ConfigError::MissingClientSecret + })?; + + let allowed_domains = get_env(ENV_GOOGLE_ALLOWED_DOMAINS)?.map_or(Vec::new(), |d| { + d.split(',').map(str::to_string).collect::>() + }); + + let auth_url = + get_env(ENV_GOOGLE_AUTH_URL)?.unwrap_or_else(|| DEFAULT_GOOGLE_AUTH_URL.to_string()); + + let service_account_file = get_env(ENV_GOOGLE_SERVICE_ACCOUNT)? + .unwrap_or_else(|| DEFAULT_GOOGLE_SERVICE_ACCOUNT.to_string()) + .into(); + + let admin_email = get_env(ENV_GOOGLE_ADMIN_EMAIL)?.ok_or_else(|| { + error!("expected Google admin email"); + ConfigError::MissingAdminEmail + })?; + + GoogleConfig { + client_id, + client_secret, + allowed_domains, + auth_url, + service_account_file, + admin_email, + } + }; -fn default_cookie_name() -> String { - DEFAULT_COOKIE_NAME.to_string() + Ok(Config { + addr, + cookie, + google, + }) } -fn default_auth_url() -> String { - DEFAULT_AUTH_URL.to_string() +fn get_env(key: &str) -> Result, ConfigError> { + match var(key) { + Ok(value) => Ok(Some(value)), + Err(VarError::NotPresent) => Ok(None), + Err(VarError::NotUnicode(_)) => { + error!("{} is not unicode", key); + Err(ConfigError::InvalidEnvVar) + } + } } diff --git a/src/main.rs b/src/main.rs index 0b9a039..8d381ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,22 @@ +//! poser is a simple, opinionated authentication provider for nginx +//! +//! poser authenticates with Google using OpenID Connect and then uses the +//! Google Workspace Admin SDK to determine what groups a user is a part of. +//! Basic information about the users, as well as what groups they are a part +//! of, is returned to nginx in a PASETO v4 token, which is then passed to the +//! application. + mod config; mod error; mod oidc; mod routes; use std::env::var; -use std::fs::read_to_string; -use std::net::SocketAddr; -use std::path::PathBuf; use config::Config; use routes::routes; -use anyhow::{Context, Result}; use axum::Server; -use clap::Parser; use tower::ServiceBuilder; use tower_cookies::CookieManagerLayer; use tower_http::{ @@ -22,39 +25,18 @@ use tower_http::{ }; use tracing::{info, Level}; -#[derive(Parser, Debug)] -#[command(author, version)] -/// poser is a simple, opinionated authentication provider for nginx -/// -/// poser authenticates with Google using OpenID Connect and then uses the -/// Google Workspace Admin SDK to determine what groups a user is a part of. -/// Basic information about the users as well as what groups they are a part -/// of is returned to nginx in a PASETO v4 token, which is then passed to the -/// application. -struct Args { - /// Configuration file - #[arg(long, default_value = "/data/config/config.toml")] - config: PathBuf, -} - #[derive(Debug, Clone)] pub struct ServerState { pub config: Config, } #[tokio::main] -async fn main() -> Result<()> { - init_logging(); - - let args = Args::parse(); +async fn main() { + tracing_subscriber::fmt() + .with_env_filter(var("RUST_LOG").unwrap_or_else(|_| "info".to_string())) + .init(); - let config: Config = toml::from_str(&read_to_string(&args.config).with_context(|| { - format!( - "Failed to read config file at {}", - args.config.to_string_lossy() - ) - })?) - .context("Failed to parse config file")?; + let config = Config::try_env().expect("invalid configuration"); let state = ServerState { config: config.clone(), @@ -73,18 +55,8 @@ async fn main() -> Result<()> { ) .layer(CookieManagerLayer::new()), ); + let server = Server::bind(&config.addr).serve(app.into_make_service()); - let server = - Server::bind(&SocketAddr::from((config.ip, config.port))).serve(app.into_make_service()); - - info!("Serving on {}:{}", config.ip, config.port); - server.await.context("Server unexpectedly stopped")?; - - Ok(()) -} - -fn init_logging() { - tracing_subscriber::fmt() - .with_env_filter(var("RUST_LOG").unwrap_or_else(|_| "info".to_string())) - .init(); + info!("serving on {}", config.addr); + server.await.expect("server unexpectedly stopped"); }