diff options
| author | benj <benj@rse8.com> | 2022-12-01 10:30:34 -0800 |
|---|---|---|
| committer | benj <benj@rse8.com> | 2022-12-01 10:35:50 -0800 |
| commit | 2c4eb2d311919ad9fb70738199ecf99bf20c9fce (patch) | |
| tree | 8739dd9d1d0c07fc27df2ece3d21f3a03db7397b /crates/iam/src | |
| parent | aa8c20d501b58001a5e1b24964c62363e2112ff8 (diff) | |
| download | secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.gz secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.bz2 secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.lz secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.xz secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.zst secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.zip | |
- basic functionality with psql and sqlite
- cli helper tool
Diffstat (limited to '')
| -rw-r--r-- | crates/iam/src/api.rs | 498 | ||||
| -rw-r--r-- | crates/iam/src/command.rs | 164 | ||||
| -rw-r--r-- | crates/iam/src/main.rs | 287 | ||||
| -rw-r--r-- | crates/iam/src/util.rs | 88 |
4 files changed, 1037 insertions, 0 deletions
diff --git a/crates/iam/src/api.rs b/crates/iam/src/api.rs new file mode 100644 index 0000000..5819533 --- /dev/null +++ b/crates/iam/src/api.rs @@ -0,0 +1,498 @@ +use crate::ISSUE_TRACKER_LOC; +use clap::{Parser, Subcommand, ValueEnum}; +use colored::*; +use serde::{Deserialize, Serialize}; +use thiserror; +use uuid::Uuid; + +#[derive(Debug, thiserror::Error)] +pub enum CliError { + #[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)] +#[command( + name = "iam", + author = "benabel", + version = "0.0.1", + long_about = "SecD IAM\nIdentity and access management secured by secd and user controlled. Get started with `iam init`" +)] +pub struct Args { + #[command(subcommand)] + pub command: Command, + /// IAM backend profile as defined in the iam config or as a connection string to an auth store. + #[arg(long, short)] + pub profile: Option<String>, + /// The store type for the associated connection string + #[arg(long, short)] + pub store_type: Option<StoreType>, + /// Connection string for the IAM store + #[arg(long, short)] + pub connection_string: Option<String>, +} + +#[derive(Clone, ValueEnum)] +pub enum StoreType { + Dynamo, + Memory, + Mysql, + Postgres, + Redis, + Sqlite, +} + +#[derive(Subcommand)] +pub enum Command { + #[command( + about = "Administrative actions for this IAM instance", + long_about = "Admin\n\nAdministrative actions for this IAM instance. Each IAM instance is defined by the specified backend in the iam config or manually as an optional argument" + )] + Admin { + #[command(subcommand)] + action: AdminAction, + }, + #[command( + about = "Create a new IAM object which may be used to enforce authorization schemes.", + long_about = "Create\n\nEntities which define the structure of an IAM instance. These entities may be rendered as an IAM graph, or within a web view, to more easily visualize and manipulate the IAM instance." + )] + Create { + #[command(subcommand)] + object: CreateObject, + }, + #[command( + about = "Get details for a specific IAM object", + long_about = "Get\n\nGet details for a specific IAM object" + )] + Get { + #[command(subcommand)] + object: GetObject, + }, + #[command( + about = "Initialize an IAM store (alias for `iam admin init`)", + long_about = "Init\n\nInitalize a new IAM admin store and save the store's configuration profile. This command is an alias for, and thus equiavlent to, `iam admin init`." + )] + Init { + /// If true, interactively initialize an IAM store. Otherwise output a template config. + #[arg(long, short, action)] + interactive: bool, + }, + #[command( + about = "Link multiple IAM objects together", + long_about = "Link\n\nCleave different IAM entities to create an IAM system." + )] + Link { + #[command(subcommand)] + object: LinkObject, + /// Unlink the provided entities rather than link them. + #[arg(long, short, action)] + unlink: bool, + }, + #[command( + about = "List and filter IAM objects", + long_about = "List\n\nPage through collections of IAM objects with optional filtering" + )] + Ls { + #[command(subcommand)] + object: ListObject, + /// Regex filter for entity names + #[arg(long, short)] + name: Option<String>, + /// Only fetch entities created after this time + #[arg(long, short)] + after: Option<i64>, + /// Only fetch entities created before this time + #[arg(long, short)] + before: Option<i64>, + }, + /// Start the iam repl to more easily interact with iam and its primitives + Repl, +} + +#[derive(Subcommand)] +pub enum AdminAction { + /// Aliased as `iam init` + Init { + /// If true, interactively initialize an IAM store. Otherwise output a template config. + #[arg(long, short, action)] + interactive: bool, + }, + /// Configure, describe, or rotate the default IAM store. + Backend { + #[command(subcommand)] + action: AdminBackendAction, + }, + /// Create a new administrative entity for an IAM store. + Create { + #[command(subcommand)] + object: AdminObject, + }, + /// Seal the configured IAM store to prevent administrative changes + Seal, + /// Unseal the configured IAM store to make administrative changes + Unseal { + /// The secret key used to seal this store + secret_key: String, + }, +} + +#[derive(Subcommand)] +pub enum AdminBackendAction { + Configure { + name: String, + store: StoreType, + connection: String, + }, + Switch { + name: String, + }, +} + +#[derive(Subcommand)] +pub enum AdminObject { + /// An email template used for IAM procedures, including identity validation + EmailTemplate { + template_type: EmailTemplateType, + template: String, + }, + /// A selected provider capable of sending email messages + EmailProvider { + provider: EmailProvider, + secret_key: String, + public_key: Option<String>, + }, + /// A selected Oauth2.0 provider capable of authenticating identities + OauthProvider { + provider: OauthProvider, + client_id: String, + secret: String, + redirect_uri: String, + }, + /// A selected provider capable of sending SMS + SmsProvider { + provider: SmsProvider, + secret_key: String, + public_key: Option<String>, + }, + /// A new secret which may be used to unseal the IAM store + StoreSecret, + /// A selected provider capable of sending automated voice messages + VoiceProvider { + provider: VoiceProvider, + secret_key: String, + public_key: Option<String>, + }, +} + +#[derive(Clone, ValueEnum)] +pub enum EmailTemplateType { + Login, + SignUp, +} + +#[derive(Clone, ValueEnum)] +pub enum EmailProvider { + Custom, + Mailgun, + Sendgrid, + Ses, +} + +#[derive(Clone, ValueEnum)] +pub enum SmsProvider { + AwsSns, + Custom, + Twilio, +} + +#[derive(Clone, ValueEnum)] +pub enum VoiceProvider { + Custom, + Twilio, +} + +#[derive(Subcommand)] +pub enum CreateObject { + #[command( + about = "A set of long-lived tokens which authorize an identity", + long_about = "Api Keys\n\nApi keys are long lived identifiers which authenticate and authorize a identity. Keys have a public and private part,\nwhich may be shared and must be kept private, respectively. Unlike sessions, api keys may be long-lived (infinite) or\nset to expire within certain timeframes." + )] + ApiKey { + /// Identity against which this api key will be linked + identity: Uuid, + /// Time this api key expires (epoch time) + expires_at: Option<i64>, + }, + #[command( + about = "A collection of identities", + long_about = "Group\n\nA group may be created to operate simultaneously against a collection of identities. An identity may be part of mutliple groups, but it may not be part of the same group more than once." + )] + Group { + /// The unique name for this group + name: String, + /// An optional set of identities to link against this group + identities: Vec<Uuid>, + }, + #[command( + about = "A collection of services and service actions", + long_about = "Permission\n\nA permission may be created to operate simultaneously against a collection of services and service actions. A service or service action may be part of mutliple permissions, but it may not be part of the same permission more than once. A permission may be used when many services and service actions are linked and unlinked against a role." + )] + Permission { + /// An optional set of services to link against this permission + #[arg(long, short)] + services: Vec<Uuid>, + /// An optional set of service actions to link against this permission + #[arg(long, short)] + actions: Vec<Uuid>, + }, + #[command( + about = "A collection of permissions", + long_about = "Role\n\nA role may be created to operate simultaneously against a collection of permissions. A permission may be part of mutliple roles, but it may not be part of the same role more than once. A role may be used when many entities (such as groups or identities) are linked and unlinked against many permissions." + )] + Role { + /// The unique name for this role + name: String, + /// An optional set of permissions to link against this role + permissions: Vec<Uuid>, + }, + #[command( + about = "An entity for which an action may be authorized", + long_about = "Service\n\nA service is an atomic entity which requires authorization. While a service's authorization may be subdivided by service actions, a service represents a logical element of authorization separation." + )] + Service { + /// The unique name for this service + name: String, + /// URI for this service which may be used to resolve authorization + #[arg(long, short)] + uri: Option<String>, + }, + #[command( + about = "A specific authorization action by a service", + long_about = "Service Action\n\nA service action is a domain specific action which defines what an identity authorization within that service. A service action may be a simple boolean value or a more complex express which is evaluated at runtime. For example, a boolean action may be something like `can_read_salary_table`, and a more complex action may be `readable_table_rows(datetime)` which executes at runtime and returns a value (or list of values) the service may use to determine authorization. Service actions are used as an inversion of control pattern to ensure that services do not need to worry about specific authorization actions for identities. A service action is unnecessary if the service has no specific authorization logic." + )] + ServiceAction { + /// The unique name for this service action + name: String, + /// Program executed for this service action + #[arg(long, short)] + program: Option<String>, + }, + #[command( + about = "A timebound token which authorizes an identity", + long_about = "Session\n\nA session is an opaque timebound token which allows an identity to authorize against IAM services. The session may be created by providing a validation request id and secret challenge code" + )] + Session { + /// The validation id associated with a non-expired valid validation + #[arg(long, short)] + validation_id: Uuid, + /// The secret code associated with this validation. + #[arg(long, short)] + secret_code: String, + }, + #[command( + about = "An action which initiates an identity validation", + long_about = "Validation\n\nA validation requires that the identity authenticate in some way, either by providing IAM managed credentials, an external gated mechanism (e.g. email, phone, or hardware key), or through a secondary authentication provider (oauth, saml, ldap, kerberos)." + )] + Validation { + /// Method by which the validation will occur + #[command(subcommand)] + method: ValidationMethod, + /// The identity against which to associate this validation. A new identity will be created if no identity is provided. + #[arg(long, short)] + identity: Option<Uuid>, + }, +} + +#[derive(Subcommand)] +pub enum ValidationMethod { + /// An email address to which the validation will be sent + Email { + /// Email address which will receive the validation + address: String, + }, + /// A hardware security key to associate with an identity + HardwareKey, + /// A kerberos ticket to associated with an identity + Kerberos, + /// An oauth2 provider to authenticate (and authorize) an identity + Oauth2 { + provider: OauthProvider, + /// An optional scope to use for authorization + scope: Option<String>, + }, + /// A phone which an identity may authenticate via SMS or voice + Phone { + /// Whether to use a voice code. Otherwise, uses SMS + #[arg(long, short, action)] + use_voice: bool, + }, + /// A saml provider to authenticate an identity + 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 { + /// Public key associated with this api key set + public_key: String, + }, + Group { + /// Unique group name + name: String, + /// Unique group id + #[arg(long, short)] + id: Option<Uuid>, + }, + Identity { + /// Unique identity id + id: Uuid, + }, + Permission { + /// Unique permission name + name: String, + /// Unique permission id + #[arg(long, short)] + id: Option<Uuid>, + }, + Role { + /// Unique role name + name: String, + /// Unique role id + #[arg(long, short)] + id: Option<Uuid>, + }, + Session { + /// The plaintext token which uniquely identifies the session + secret: String, + }, + Service { + /// Unique service name + name: String, + /// Unique service id + #[arg(long, short)] + id: Option<Uuid>, + }, + ServiceAction { + /// Unique service action name + name: String, + /// Unique service action id + #[arg(long, short)] + id: Option<Uuid>, + }, + Validation { + /// Unique validation request id + id: Uuid, + }, +} + +#[derive(Subcommand)] +pub enum LinkObject { + Group { + group_name: String, + #[arg(short, long, alias = "id")] + group_id: Option<Uuid>, + + identity_ids: Vec<Uuid>, + }, + Identity { + identity_id: Uuid, + + group_names: Vec<String>, + #[arg(long)] + group_ids: Vec<Uuid>, + }, + Permission { + permission_name: String, + #[arg(short, long, alias = "id")] + permission_id: Option<Uuid>, + + role_names: Vec<String>, + #[arg(long)] + role_ids: Vec<Uuid>, + }, + Role { + role_name: String, + #[arg(short, long, alias = "id")] + role_id: Option<Uuid>, + + permission_names: Vec<String>, + #[arg(long)] + permission_ids: Vec<Uuid>, + }, + Service { + service_name: String, + #[arg(short, long, alias = "id")] + service_id: Option<Uuid>, + + permission_names: Vec<String>, + #[arg(long)] + permission_ids: Vec<Uuid>, + }, + ServiceAction { + service_action_name: String, + #[arg(short, long, alias = "id")] + service_action_id: Option<Uuid>, + + service_name: Vec<String>, + #[arg(long)] + service_ids: Vec<Uuid>, + }, +} + +#[derive(Subcommand)] +pub enum ListObject { + ApiKey, + Group, + Identity, + Permission, + Role, + Session, + Service, + ServiceAction, + Validation, +} + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub profile: Vec<ConfigProfile>, +} + +#[derive(Serialize, Deserialize)] +pub struct ConfigProfile { + pub name: String, + pub store: secd::AuthStore, + pub store_conn: String, + pub emailer: secd::AuthEmail, + pub email_template_login: Option<String>, + pub email_template_signup: Option<String>, +} diff --git a/crates/iam/src/command.rs b/crates/iam/src/command.rs new file mode 100644 index 0000000..e9e0f23 --- /dev/null +++ b/crates/iam/src/command.rs @@ -0,0 +1,164 @@ +use crate::{ + api, + util::{self, get_config_profile, Result}, + CONFIG_LOGIN_TEMPLATE, CONFIG_SIGNUP_TEMPLATE, +}; +use async_std::fs; +use colored::*; +use rand::distributions::{Alphanumeric, DistString}; +use secd::{AuthEmail, AuthStore}; +use std::{ + fs::File, + io::{self, stdin, stdout, Write}, + str::FromStr, +}; +use strum::VariantNames; + +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>"; + +pub async fn admin_init(is_interactive: bool) -> Result<()> { + let config_dir = util::get_config_dir(); + let config_profile = get_config_profile(); + fs::create_dir_all(config_dir.clone()).await?; + + if config_profile.try_exists()? { + writeln!( + io::stdout(), + "{} {}", + config_profile.clone().display(), + "already exists and there is nothing to initialize. To create a new IAM store use `iam admin create store` or modify the configuration profile directly" + .yellow() + )?; + } else { + writeln!(stdout(), "{}", "creating default profile".green())?; + + let mut login_template = config_dir.clone(); + login_template.push(CONFIG_LOGIN_TEMPLATE); + let mut f = File::create(login_template.clone())?; + f.write_all(DEFAULT_LOGIN_EMAIL.as_bytes())?; + + let mut signup_template = config_dir.clone(); + signup_template.push(CONFIG_SIGNUP_TEMPLATE); + f = File::create(signup_template.clone())?; + f.write_all(DEFAULT_SIGNUP_EMAIL.as_bytes())?; + + let mut cfg = api::Config { + profile: vec![api::ConfigProfile { + name: "default".to_string(), + store: AuthStore::Sqlite, + store_conn: format!( + "sqlite://{}/{}.sql?mode=rwc", + config_dir.clone().display().to_string(), + Alphanumeric.sample_string(&mut rand::thread_rng(), 5), + ), + emailer: secd::AuthEmail::LocalStub, + email_template_login: Some(login_template.display().to_string()), + email_template_signup: Some(signup_template.display().to_string()), + }], + }; + + let mut input: String = String::new(); + if is_interactive { + writeln!(stdout(), "{}", "For a complete overview of configuration options, cancel the initialization and explore `iam help`")?; + write!(stdout(), "Would you like to create a default local store with local stubs for external services?[(y)es/(n)o]: ")?; + stdout().flush()?; + 'outer: loop { + input.clear(); + stdin().read_line(&mut input)?; + match input.as_str().trim() { + "Y" | "y" | "Yes" | "yes" => break, + "N" | "n" | "No" | "no" => { + loop { + write!( + stdout(), + "Persistence store {:?}: ", + AuthStore::VARIANTS + .iter() + .map(|s| s.to_lowercase()) + .collect::<Vec<String>>() + )?; + stdout().flush()?; + input.clear(); + stdin().read_line(&mut input)?; + match AuthStore::from_str(&input.trim()) { + Ok(s) => { + cfg.profile[0].store = s; + break; + } + Err(_) => { + writeln!(stdout(), "{}", "Invalid store type".red())?; + } + } + } + + write!(stdout(), "Store connection string: ")?; + stdout().flush()?; + input.clear(); + stdin().read_line(&mut input)?; + cfg.profile[0].store_conn = input.trim().to_string().clone(); + + loop { + write!( + stdout(), + "Email provider {:?}: ", + AuthEmail::VARIANTS + .iter() + .map(|s| s.to_lowercase()) + .collect::<Vec<String>>() + )?; + stdout().flush()?; + input.clear(); + stdin().read_line(&mut input)?; + match AuthEmail::from_str(&input.trim()) { + Ok(s) => { + cfg.profile[0].emailer = s; + break; + } + Err(_) => { + writeln!(stdout(), "{}", "Invalid email provider".red())?; + } + } + } + + write!( + stdout(), + "Email template for login validation:[FilePath or Enter for default]: " + )?; + stdout().flush()?; + input.clear(); + stdin().read_line(&mut input)?; + cfg.profile[0].email_template_login = + Some(input.trim().to_string().clone()); + + write!( + stdout(), + "Email template for signup validation:[FilePath or Enter for default]: " + )?; + stdout().flush()?; + input.clear(); + stdin().read_line(&mut input)?; + cfg.profile[0].email_template_login = + Some(input.trim().to_string().clone()); + + break 'outer; + } + _ => {} + } + } + } + + let mut f = File::create(config_profile.clone())?; + f.write_all(toml::to_string(&cfg)?.as_bytes())?; + writeln!( + stdout(), + "{} {} {} {} {}", + "created iam config".green(), + "default", + "at".green(), + config_dir.display().to_string(), + "to hold secD iam configurations".green() + )?; + } + Ok(()) +} diff --git a/crates/iam/src/main.rs b/crates/iam/src/main.rs new file mode 100644 index 0000000..c187380 --- /dev/null +++ b/crates/iam/src/main.rs @@ -0,0 +1,287 @@ +mod api; +mod command; +mod util; + +use api::{AdminAction, Args, CliError, Command, CreateObject, GetObject, LinkObject, ListObject}; +use clap::Parser; +use secd::{Secd, SecdError}; +use util::Result; + +use crate::api::ValidationMethod; + +const CONFIG_DIR_NAME: &str = "secdiam"; +const CONFIG_PROFILE_FILE: &str = "profiles.toml"; +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] +async fn main() { + match exec().await { + Ok(Some(s)) => println!("{}", s), + Err(e) => { + println!("{}", e); + std::process::exit(1); + } + _ => {} + } +} + +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?, + + rest @ _ => { + let cfg = util::read_config(args.profile).map_err(|_| CliError::InvalidProfile)?; + let secd = Secd::init( + cfg.store, + Some(&cfg.store_conn), + cfg.emailer, + cfg.email_template_login, + cfg.email_template_signup, + ) + .await + .map_err(|e| CliError::SecdInitializationFailure(e.to_string()))?; + + match rest { + Command::Create { object } => create(&secd, object).await?, + Command::Get { object } => get(&secd, object).await?, + Command::Link { object, unlink } => link(&secd, object, unlink).await?, + Command::Ls { + object, + name, + before, + after, + } => list(&secd, object, name, before, after).await?, + Command::Repl => { + unimplemented!() + } + _ => None, + } + } + }) +} + +async fn admin(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::Seal => { + println!("do seal"); + None + } + AdminAction::Unseal { secret_key } => { + println!("do unseal: {}", secret_key); + None + } + }) +} +async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> { + Ok(match cmd { + CreateObject::ApiKey { + identity, + expires_at, + } => { + println!("create object"); + None + } + CreateObject::Group { name, identities } => { + println!("create group"); + None + } + CreateObject::Permission { services, actions } => { + println!("create permission"); + None + } + CreateObject::Role { name, permissions } => { + println!("create role"); + None + } + CreateObject::Service { name, uri } => { + println!("create service"); + None + } + CreateObject::ServiceAction { name, program } => { + println!("create service action"); + None + } + CreateObject::Session { + validation_id, + secret_code, + } => { + let session = secd + .exchange_code_for_session(validation_id, secret_code) + .await + .map_err(|e| match e { + SecdError::InvalidCode => CliError::InvalidCode, + _ => CliError::Unknown, + })?; + serde_json::to_string(&session).ok() + } + CreateObject::Validation { method, identity } => match method { + ValidationMethod::Email { address } => { + secd.create_validation_request(Some(&address)).await?; + None + } + _ => unimplemented!(), + }, + }) +} +async fn get(secd: &Secd, cmd: GetObject) -> Result<Option<String>> { + Ok(match cmd { + GetObject::ApiKey { public_key } => { + println!("get object api key"); + None + } + GetObject::Group { name, id } => { + println!("get object group"); + None + } + GetObject::Identity { id } => { + println!("get object identity"); + None + } + GetObject::Permission { name, id } => { + println!("get object permission"); + None + } + GetObject::Role { name, id } => { + println!("get object role"); + None + } + GetObject::Service { name, id } => { + println!("get object service"); + None + } + GetObject::ServiceAction { name, id } => { + println!("get object service action"); + None + } + GetObject::Session { secret } => { + println!("get object session"); + None + } + GetObject::Validation { id } => { + println!("get object validation"); + None + } + }) +} +async fn link(secd: &Secd, cmd: LinkObject, should_unlink: bool) -> Result<Option<String>> { + Ok(match cmd { + LinkObject::Group { + group_name, + group_id, + identity_ids, + } => { + println!("link object group"); + None + } + LinkObject::Identity { + identity_id, + group_names, + group_ids, + } => { + println!("link object identity"); + None + } + LinkObject::Permission { + permission_name, + permission_id, + role_names, + role_ids, + } => { + println!("link object permission"); + None + } + LinkObject::Role { + role_name, + role_id, + permission_names, + permission_ids, + } => { + println!("link object role"); + None + } + LinkObject::Service { + service_name, + service_id, + permission_names, + permission_ids, + } => { + println!("link object service"); + None + } + LinkObject::ServiceAction { + service_action_name, + service_action_id, + service_name, + service_ids, + } => { + println!("link object service action"); + None + } + }) +} +async fn list( + secd: &Secd, + cmd: ListObject, + filter_name: Option<String>, + filter_before: Option<i64>, + filter_after: Option<i64>, +) -> Result<Option<String>> { + Ok(match cmd { + ListObject::ApiKey => { + println!("list object api key"); + None + } + ListObject::Group => { + println!("list object group"); + None + } + ListObject::Identity => { + println!("list object identity"); + None + } + ListObject::Permission => { + println!("list object permission"); + None + } + ListObject::Role => { + println!("list object role"); + None + } + ListObject::Service => { + println!("list object service"); + None + } + ListObject::ServiceAction => { + println!("list object service action"); + None + } + ListObject::Session => { + println!("list object session"); + None + } + ListObject::Validation => { + println!("list object valiation"); + None + } + }) +} diff --git a/crates/iam/src/util.rs b/crates/iam/src/util.rs new file mode 100644 index 0000000..01ce851 --- /dev/null +++ b/crates/iam/src/util.rs @@ -0,0 +1,88 @@ +use crate::{ + api::{CliError, Config, ConfigProfile}, + CONFIG_DIR_NAME, CONFIG_PROFILE_FILE, ISSUE_TRACKER_LOC, +}; +use anyhow::{anyhow, Context}; +use colored::Colorize; +use home::home_dir; +use secd::Secd; +use std::{ + env::var, + error::Error, + fs::{self, File}, + io::{self, Read}, + path::PathBuf, + result, + str::FromStr, +}; +use thiserror; + +pub type Result<T> = anyhow::Result<T>; + +macro_rules! err { + ($($tt:tt)*) => { Err(Box::<dyn Error>::from(format!($($tt)*))) } +} +pub(crate) use err; + +#[derive(Debug, thiserror::Error)] +pub enum InternalError { + #[error( + "Cannot read {0} profile from {1}. Initialize a default iam store with `iam admin init`" + )] + CannotReadProfile(String, String), +} + +pub fn get_config_dir() -> PathBuf { + let xdg_dir = var("XDG_CONFIG_HOME").map(|s| PathBuf::from_str(&s).unwrap()); + let mut home_dir = home_dir().expect(&format!( + "Could not find home directory. This should not be possible, please file a bug at {}", + ISSUE_TRACKER_LOC + )); + + match xdg_dir { + Ok(mut d) => { + d.push(format!(".{}", CONFIG_DIR_NAME)); + d + } + Err(_) => { + home_dir.push(".config"); + home_dir.push(CONFIG_DIR_NAME); + home_dir + } + } +} + +pub fn get_config_profile() -> PathBuf { + let mut config_dir = get_config_dir(); + config_dir.push(CONFIG_PROFILE_FILE); + config_dir +} + +pub fn read_config(profile_name: Option<String>) -> Result<ConfigProfile> { + let profile_path = get_config_profile(); + let profile_name = profile_name.unwrap_or("default".into()); + + let bytes = fs::read(profile_path.clone())?; + let config: Config = toml::from_slice(&bytes)?; + + let mut cfg = config + .profile + .into_iter() + .filter(|p| p.name == profile_name) + .last() + .ok_or(anyhow!( + "cannot read configuration file when calling read_config" + ))?; + + if let Some(path) = cfg.email_template_login { + let buf = fs::read_to_string(path)?; + cfg.email_template_login = Some(buf); + } + + if let Some(path) = cfg.email_template_signup { + let buf = fs::read_to_string(path)?; + cfg.email_template_signup = Some(buf); + } + + Ok(cfg) +} |
