use crate::{ config::Config, key::{self, KeyType}, realm::RealmHelper, schema::{self, UIDCDatabase}, server, UIDCError, }; use clap::{Parser, Subcommand}; use microrm::cli::Autogenerate; use microrm::{prelude::*, schema::Stored}; mod client; mod group; mod role; mod scope; mod user; impl microrm::cli::CLIError for UIDCError { fn no_such_entity(ename: &'static str, keys: String) -> Self { UIDCError::AbortString(format!("no such {ename} matching {keys}")) } } #[derive(Debug, Parser)] #[clap(author, version, about, long_about = None)] struct RootArgs { #[clap(short, long, env = "UIDC_CONFIG", default_value_t = String::from("uidc.toml"))] /// Configuration file path config_path: String, #[clap(short, long, default_value_t = String::from("primary"))] /// Which realm to use, for non-serve commands only realm: String, #[clap(subcommand)] command: Command, } #[derive(Debug, Subcommand)] enum Command { /// database initialization Init, /// OAuth2 client management Client { #[clap(subcommand)] cmd: Autogenerate, }, // /// general configuration // Config(ConfigArgs), /// permissions grouping management Group { #[clap(subcommand)] cmd: Autogenerate, }, /// key management Key(KeyArgs), /// scope management Scope { #[clap(subcommand)] cmd: Autogenerate, }, /// run the actual OIDC server Serve(ServerArgs), /// manual token generation and inspection Token { #[clap(subcommand)] cmd: TokenCommand, }, /// role management Role { #[clap(subcommand)] cmd: Autogenerate, }, /// user management User { #[clap(subcommand)] cmd: Autogenerate, }, } pub struct RunArgs { pub config: Config, pub db: UIDCDatabase, pub realm: Stored, } impl RootArgs { async fn run(self) -> Result<(), UIDCError> { let config_contents = std::fs::read_to_string(self.config_path.as_str()) .expect("couldn't open configuration file"); let config: Config = toml::from_str(config_contents.as_str()).expect("couldn't parse configuration file"); if let Command::Init = self.command { return self.init(config).await; } let db = UIDCDatabase::open_path(&config.db_path) .map_err(|e| UIDCError::AbortString(format!("Error accessing database: {:?}", e)))?; let realm = db .realms .keyed(&self.realm) .get()? .ok_or(UIDCError::Abort("no such realm"))?; let ra = RunArgs { config, db, realm }; match self.command { Command::Init => unreachable!(), // Command::Config(v) => v.run(ra).await, Command::Key(v) => v.run(ra).await, Command::Client { cmd } => cmd.perform(&ra.realm, &ra.realm.clients), Command::Scope { cmd } => cmd.perform(&ra.realm, &ra.realm.scopes), Command::Group { cmd } => cmd.perform(&ra.realm, &ra.realm.groups), Command::Serve(v) => v.run(ra).await, Command::Token { cmd } => cmd.run(ra).await, Command::Role { cmd } => cmd.perform(&ra.realm, &ra.realm.roles), Command::User { cmd } => cmd.perform(&ra, &ra.realm.users), } } async fn init(&self, config: Config) -> Result<(), UIDCError> { // first check to see if the database is already vaguely set up let db = UIDCDatabase::open_path(&config.db_path) .map_err(|e| UIDCError::AbortString(format!("Error accessing database: {:?}", e)))?; log::info!("Initializing!"); if db.realms.keyed("primary").get()?.is_some() { log::warn!("Already initialized with primary realm!"); return Ok(()); } // create primary realm let primary = db.realms.insert_and_return(schema::Realm { shortname: "primary".into(), ..Default::default() })?; // add a HMAC key for refresh tokens key::generate_in(&primary, KeyType::HMac(key::HMacType::Sha256))?; Ok(()) } } #[derive(Debug, Subcommand)] enum KeyCommand { /// Print details of all keys List, /// Generate a new key; see types subcommand for valid options. Generate { key_type: KeyType }, /// List what keytypes are supported Types, /// Remove a key from use Remove { key_id: String }, } #[derive(Debug, Parser)] struct KeyArgs { #[clap(subcommand)] command: KeyCommand, } impl KeyArgs { async fn run(self, args: RunArgs) -> Result<(), UIDCError> { match &self.command { KeyCommand::List => key::list(&args.realm), KeyCommand::Generate { key_type } => { key::generate_in(&args.realm, *key_type)?; Ok(()) } KeyCommand::Types => { for (spec, _kty) in key::KEY_TYPE_NAMES { println!("- {}", spec); } Ok(()) } KeyCommand::Remove { key_id } => key::remove(&args.realm, key_id), } } } /* #[derive(Debug, Subcommand)] enum ConfigCommand { Dump, Load { toml_path: String }, Set { key: String, value: String }, } #[derive(Debug, Parser)] struct ConfigArgs { #[clap(subcommand)] command: ConfigCommand, } impl ConfigArgs { async fn run(self, args: RunArgs) -> Result<(), UIDCError> { match &self.command { ConfigCommand::Dump => { let config = config::Config::build_from(&args.db, None); println!("{:#?}", config); } ConfigCommand::Set { key, value } => { args.db.persistent_config.keyed(key).delete()?; args.db.persistent_config.insert(schema::PersistentConfig { key: key.clone(), value: value.clone(), })?; } ConfigCommand::Load { toml_path } => { let config = config::Config::build_from(&args.db, Some(toml_path)); config.save(&args.db); } } Ok(()) } } */ #[derive(Debug, Parser)] struct ServerArgs { #[clap(short, long)] bind: Option, #[clap(short, long)] port: Option, } impl ServerArgs { async fn run(self, args: RunArgs) -> Result<(), UIDCError> { server::run_server( args.db, args.config, self.bind.as_deref().unwrap_or("127.0.0.1"), self.port.unwrap_or(2114), ) .await } } #[derive(Debug, Subcommand)] enum TokenCommand { GenerateAuth { #[clap(short, long)] client: String, #[clap(short, long)] username: String, #[clap(short, long)] scopes: String, }, GenerateRefresh { #[clap(short, long)] client: String, #[clap(short, long)] username: String, #[clap(short, long)] scopes: String, }, Inspect { token: Option, }, } impl TokenCommand { async fn run(self, args: RunArgs) -> Result<(), UIDCError> { let get_stored = |client_name: &str, user_name: &str| { let stored_client = args .realm .clients .with(schema::Client::Shortname, client_name) .first() .get()? .ok_or(UIDCError::Abort("no such client"))?; let stored_user = args .realm .users .with(schema::User::Username, user_name) .first() .get()? .ok_or(UIDCError::Abort("no such user"))?; Result::<_, UIDCError>::Ok((stored_client, stored_user)) }; match self { TokenCommand::GenerateAuth { client, username, scopes, } => { let (stored_client, stored_user) = get_stored(client.as_str(), username.as_str())?; let realm = RealmHelper::new(args.config, args.realm); let token = realm.generate_access_token(&stored_client, &stored_user, scopes.split(' '))?; println!("{}", token); Ok(()) } TokenCommand::GenerateRefresh { client, username, scopes, } => { let (stored_client, stored_user) = get_stored(client.as_str(), username.as_str())?; let realm = RealmHelper::new(args.config, args.realm); let token = realm.generate_refresh_token( &stored_client, &stored_user, scopes.split(' '), )?; println!("{}", token); Ok(()) } TokenCommand::Inspect { token: _ } => { todo!() // token_management::inspect_token(&config, &args.realm, token.as_ref()) } } } } pub fn invoked() { let args = RootArgs::parse(); match smol::block_on(args.run()) { Ok(_) => (), Err(e) => { log::error!("Error occured while running command: {}", e); } } }