diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 99cfc35..b3e76b5 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,6 +29,8 @@ jobs: - uses: actions/checkout@v3 - name: Build run: cargo build --verbose + - name: ulimit -n + run: ulimit -n 65535 - name: Run tests run: cargo test --verbose @@ -83,6 +85,8 @@ jobs: run: diesel setup --database-url mysql://root:password@127.0.0.1:3306/vault - name: Build run: cargo build --features storage_mysql --verbose + - name: ulimit -n + run: ulimit -n 65535 - name: Run tests run: cargo test --verbose - name: Build crate doc diff --git a/src/core.rs b/src/core.rs index b63933a..41c459a 100644 --- a/src/core.rs +++ b/src/core.rs @@ -24,7 +24,14 @@ use crate::{ handler::Handler, logical::{Backend, Request, Response}, module_manager::ModuleManager, - modules::{auth::AuthModule, credential::userpass::UserPassModule, pki::PkiModule}, + modules::{ + auth::AuthModule, + credential::{ + userpass::UserPassModule, + approle::AppRoleModule, + }, + pki::PkiModule, + }, mount::MountTable, router::Router, shamir::{ShamirSecret, SHAMIR_OVERHEAD}, @@ -113,6 +120,10 @@ impl Core { let userpass_module = UserPassModule::new(self); self.module_manager.add_module(Arc::new(RwLock::new(Box::new(userpass_module))))?; + // add credential module: approle + let approle_module = AppRoleModule::new(self); + self.module_manager.add_module(Arc::new(RwLock::new(Box::new(approle_module))))?; + Ok(()) } diff --git a/src/modules/credential/approle/mod.rs b/src/modules/credential/approle/mod.rs new file mode 100644 index 0000000..6772d41 --- /dev/null +++ b/src/modules/credential/approle/mod.rs @@ -0,0 +1,597 @@ +use std::sync::{atomic::AtomicU32, Arc, RwLock}; + +use as_any::Downcast; +use derive_more::Deref; + +use crate::{ + core::Core, + errors::RvError, + logical::{Backend, LogicalBackend, Request, Response}, + modules::{auth::AuthModule, Module}, + new_logical_backend, new_logical_backend_internal, + utils::{locks::Locks, salt::Salt}, +}; + +pub mod path_login; +pub mod path_role; +pub mod path_tidy_secret_id; +pub mod validation; + +const HMAC_INPUT_LEN_MAX: usize = 4096; + +const SECRET_ID_PREFIX: &str = "secret_id/"; +const SECRET_ID_LOCAL_PREFIX: &str = "secret_id_local/"; +const SECRET_ID_ACCESSOR_PREFIX: &str = "accessor/"; +const SECRET_ID_ACCESSOR_LOCAL_PREFIX: &str = "accessor_local/"; + +static APPROLE_BACKEND_HELP: &str = r#" +Any registered Role can authenticate itself with RustyVault. The credentials +depends on the constraints that are set on the Role. One common required +credential is the 'role_id' which is a unique identifier of the Role. +It can be retrieved from the 'role//role-id' endpoint. + +The default constraint configuration is 'bind_secret_id', which requires +the credential 'secret_id' to be presented during login. Refer to the +documentation for other types of constraints.` +"#; + +#[derive(Deref)] +pub struct AppRoleModule { + pub name: String, + #[deref] + pub backend: Arc, +} + +pub struct AppRoleBackendInner { + pub core: Arc>, + pub salt: RwLock>, + pub role_locks: Locks, + pub role_id_locks: Locks, + pub secret_id_locks: Locks, + pub secret_id_accessor_locks: Locks, + pub tidy_secret_id_cas_guard: AtomicU32, +} + +#[derive(Deref)] +pub struct AppRoleBackend { + #[deref] + pub inner: Arc, +} + +impl AppRoleBackend { + pub fn new(core: Arc>) -> Self { + Self { inner: Arc::new(AppRoleBackendInner::new(core)) } + } + + pub fn new_backend(&self) -> LogicalBackend { + let approle_backend_ref = Arc::clone(&self.inner); + + let mut backend = new_logical_backend!({ + unauth_paths: ["login"], + auth_renew_handler: approle_backend_ref.renew_path_login, + help: APPROLE_BACKEND_HELP, + }); + + let role_paths = self.role_paths(); + backend.paths.extend(role_paths.into_iter().map(Arc::new)); + backend.paths.push(Arc::new(self.login_path())); + + backend.paths.push(Arc::new(self.role_path())); + backend.paths.push(Arc::new(self.tidy_secret_id_path())); + + backend + } +} + +impl AppRoleBackendInner { + pub fn new(core: Arc>) -> Self { + Self { + core, + salt: RwLock::new(None), + role_locks: Locks::new(), + role_id_locks: Locks::new(), + secret_id_locks: Locks::new(), + secret_id_accessor_locks: Locks::new(), + tidy_secret_id_cas_guard: AtomicU32::new(0), + } + } + + pub fn renew_path_login(&self, _backend: &dyn Backend, _req: &mut Request) -> Result, RvError> { + Ok(None) + } +} + +impl AppRoleModule { + pub fn new(core: &Core) -> Self { + Self { + name: "approle".to_string(), + backend: Arc::new(AppRoleBackend::new(Arc::clone(core.self_ref.as_ref().unwrap()))), + } + } +} + +impl Module for AppRoleModule { + fn name(&self) -> String { + return self.name.clone(); + } + + fn setup(&mut self, core: &Core) -> Result<(), RvError> { + let approle = Arc::clone(&self.backend); + let approle_backend_new_func = move |_c: Arc>| -> Result, RvError> { + let mut approle_backend = approle.new_backend(); + approle_backend.init()?; + Ok(Arc::new(approle_backend)) + }; + + if let Some(module) = core.module_manager.get_module("auth") { + let auth_mod = module.read()?; + if let Some(auth_module) = auth_mod.as_ref().downcast_ref::() { + return auth_module.add_auth_backend("approle", Arc::new(approle_backend_new_func)); + } else { + log::error!("downcast auth module failed!"); + } + } else { + log::error!("get auth module failed!"); + } + + Ok(()) + } + + fn init(&mut self, core: &Core) -> Result<(), RvError> { + if core.get_system_view().is_none() { + return Err(RvError::ErrBarrierSealed); + } + + let salt = Salt::new(Some(core.get_system_storage()), None)?; + + let mut approle_salt = self.backend.inner.salt.write()?; + *approle_salt = Some(salt); + + Ok(()) + } + + fn cleanup(&mut self, core: &Core) -> Result<(), RvError> { + if let Some(module) = core.module_manager.get_module("auth") { + let auth_mod = module.read()?; + if let Some(auth_module) = auth_mod.as_ref().downcast_ref::() { + return auth_module.delete_auth_backend("approle"); + } else { + log::error!("downcast auth module failed!"); + } + } else { + log::error!("get auth module failed!"); + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use std::{ + collections::HashMap, + default::Default, + env, fs, + sync::{Arc, RwLock}, + }; + + use go_defer::defer; + use serde_json::{json, Map, Value}; + + use super::*; + use crate::{ + core::{Core, SealConfig}, + logical::{field::FieldTrait, Operation, Request}, + storage, + }; + + fn test_read_api(core: &Core, token: &str, path: &str, is_ok: bool) -> Result, RvError> { + let mut req = Request::new(path); + req.operation = Operation::Read; + req.client_token = token.to_string(); + let resp = core.handle_request(&mut req); + assert_eq!(resp.is_ok(), is_ok); + resp + } + + fn test_write_api( + core: &Core, + token: &str, + path: &str, + is_ok: bool, + data: Option>, + ) -> Result, RvError> { + let mut req = Request::new(path); + req.operation = Operation::Write; + req.client_token = token.to_string(); + req.body = data; + + let resp = core.handle_request(&mut req); + println!("resp: {:?}", resp); + assert_eq!(resp.is_ok(), is_ok); + resp + } + + fn test_mount_approle_auth(core: Arc>, token: &str, path: &str) { + let core = core.read().unwrap(); + + let auth_data = json!({ + "type": "approle", + }) + .as_object() + .unwrap() + .clone(); + + let resp = test_write_api(&core, token, format!("sys/auth/{}", path).as_str(), true, Some(auth_data)); + assert!(resp.is_ok()); + } + + fn test_read_role( + core: Arc>, + token: &str, + path: &str, + role_name: &str, + ) -> Result, RvError> { + let core = core.read().unwrap(); + + let resp = test_read_api(&core, token, format!("auth/{}/role/{}", path, role_name).as_str(), true); + assert!(resp.is_ok()); + resp + } + + fn generate_secret_id(core: Arc>, token: &str, role_name: &str) -> (String, String) { + let core = core.read().unwrap(); + let resp = + test_write_api(&core, token, format!("auth/approle/role/{}/secret-id", role_name).as_str(), true, None); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id = resp_data["secret_id"].as_str().unwrap(); + let secret_id_accessor = resp_data["secret_id_accessor"].as_str().unwrap(); + + (secret_id.to_string(), secret_id_accessor.to_string()) + } + + fn test_login( + core: Arc>, + path: &str, + role_id: &str, + secret_id: &str, + is_ok: bool, + ) -> Result, RvError> { + let core = core.read().unwrap(); + + let data = json!({ + "role_id": role_id, + "secret_id": secret_id, + }) + .as_object() + .unwrap() + .clone(); + + let mut req = Request::new(format!("auth/{}/login", path).as_str()); + req.operation = Operation::Write; + req.body = Some(data); + + let resp = core.handle_request(&mut req); + if is_ok { + assert!(resp.is_ok()); + let resp = resp.as_ref().unwrap(); + assert!(resp.is_some()); + let resp = resp.as_ref().unwrap(); + assert!(resp.auth.is_some()); + } else { + assert!(resp.is_err()); + } + + resp + } + + fn test_approle(c: Arc>, token: &str, path: &str, role_name: &str) { + let core = c.read().unwrap(); + + // Create a role + let resp = test_write_api(&core, token, format!("auth/{}/role/{}", path, role_name).as_str(), true, None); + assert!(resp.is_ok()); + + // Get the role-id + let resp = test_read_api(&core, token, format!("auth/{}/role/{}/role-id", path, role_name).as_str(), true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data; + let role_id = resp_data.unwrap()["role_id"].clone(); + let role_id = role_id.as_str().unwrap(); + + // Create a secret-id + let (secret_id, secret_id_accessor) = generate_secret_id(Arc::clone(&c), token, role_name); + + // Ensure login works + let _ = test_login(Arc::clone(&c), path, role_id, &secret_id, true); + + // Destroy secret ID accessor + let data = json!({ + "secret_id_accessor": secret_id_accessor, + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api( + &core, + token, + format!("auth/{}/role/{}/secret-id-accessor/destroy", path, role_name).as_str(), + true, + Some(data), + ); + assert!(resp.is_ok()); + + // Login again using the accessor's corresponding secret ID should fail + let _ = test_login(Arc::clone(&c), path, role_id, &secret_id, false); + + // Generate another secret ID + let (secret_id, _secret_id_accessor) = generate_secret_id(Arc::clone(&c), token, role_name); + + // Ensure login works + let _ = test_login(Arc::clone(&c), path, role_id, &secret_id, true); + + // Destroy secret ID + let data = json!({ + "secret_id": secret_id, + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api( + &core, + token, + format!("auth/{}/role/{}/secret-id/destroy", path, role_name).as_str(), + true, + Some(data), + ); + assert!(resp.is_ok()); + + // Login again using the same secret ID should fail + let _ = test_login(Arc::clone(&c), path, role_id, &secret_id, false); + + // Generate another secret ID + let (secret_id, _secret_id_accessor) = generate_secret_id(Arc::clone(&c), token, role_name); + + // Ensure login works + let _ = test_login(Arc::clone(&c), path, role_id, &secret_id, true); + + // Destroy the secret ID using lower cased role name + let data = json!({ + "secret_id": secret_id, + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api( + &core, + token, + format!("auth/{}/role/{}/secret-id/destroy", path, role_name.to_lowercase()).as_str(), + true, + Some(data), + ); + assert!(resp.is_ok()); + + // Login again using the same secret ID should fail + let _ = test_login(Arc::clone(&c), path, role_id, &secret_id, false); + + // Generate another secret ID + let (secret_id, _secret_id_accessor) = generate_secret_id(Arc::clone(&c), token, role_name); + + // Ensure login works + let _ = test_login(Arc::clone(&c), path, role_id, &secret_id, true); + + // Destroy the secret ID using upper cased role name + let data = json!({ + "secret_id": secret_id, + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api( + &core, + token, + format!("auth/{}/role/{}/secret-id/destroy", path, role_name.to_uppercase()).as_str(), + true, + Some(data), + ); + assert!(resp.is_ok()); + + // Login again using the same secret ID should fail + let _ = test_login(Arc::clone(&c), path, role_id, &secret_id, false); + + // Generate another secret ID + let (secret_id, _secret_id_accessor) = generate_secret_id(Arc::clone(&c), token, role_name); + + // Ensure login works + let _ = test_login(Arc::clone(&c), path, role_id, &secret_id, true); + + // Destroy the secret ID using mixed case name + let data = json!({ + "secret_id": secret_id, + }) + .as_object() + .unwrap() + .clone(); + let mut mixed_case_name = role_name.to_string(); + if let Some(first_char) = mixed_case_name.get_mut(0..1) { + let inverted_case_char = if first_char.chars().next().unwrap().is_uppercase() { + first_char.to_lowercase() + } else { + first_char.to_uppercase() + }; + mixed_case_name.replace_range(0..1, &inverted_case_char); + } + let resp = test_write_api( + &core, + token, + format!("auth/{}/role/{}/secret-id/destroy", path, mixed_case_name).as_str(), + true, + Some(data), + ); + assert!(resp.is_ok()); + + // Login again using the same secret ID should fail + let _ = test_login(Arc::clone(&c), path, role_id, &secret_id, false); + } + + fn test_approle_role_service(c: Arc>, token: &str, path: &str, role_name: &str) { + let core = c.read().unwrap(); + + // Create a role + let mut data = json!({ + "bind_secret_id": true, + "secret_id_num_uses": 0, + "secret_id_ttl": "10m", + "token_policies": "policy", + "token_ttl": "5m", + "token_max_ttl": "10m", + "token_num_uses": 2, + "token_type": "default", + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api( + &core, + token, + format!("auth/{}/role/{}", path, role_name).as_str(), + true, + Some(data.clone()), + ); + assert!(resp.is_ok()); + + // Get the role field + let resp = test_read_role(c.clone(), token, path, role_name); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["bind_secret_id"].as_bool().unwrap(), data["bind_secret_id"].as_bool().unwrap()); + assert_eq!(resp_data["secret_id_num_uses"].as_i64().unwrap(), data["secret_id_num_uses"].as_i64().unwrap()); + assert_eq!( + resp_data["secret_id_ttl"].as_u64().unwrap(), + data["secret_id_ttl"].as_duration().unwrap().as_secs() + ); + assert_eq!( + resp_data["token_policies"].as_comma_string_slice().unwrap(), + data["token_policies"].as_comma_string_slice().unwrap() + ); + assert_eq!(resp_data["token_ttl"].as_u64().unwrap(), data["token_ttl"].as_duration().unwrap().as_secs()); + assert_eq!( + resp_data["token_max_ttl"].as_u64().unwrap(), + data["token_max_ttl"].as_duration().unwrap().as_secs() + ); + assert_eq!(resp_data["token_num_uses"].as_i64().unwrap(), data["token_num_uses"].as_i64().unwrap()); + assert_eq!(resp_data["token_type"].as_str().unwrap(), data["token_type"].as_str().unwrap()); + + // Update the role + data["token_num_uses"] = Value::from(0); + data["token_type"] = Value::from("batch"); + let resp = test_write_api( + &core, + token, + format!("auth/{}/role/{}", path, role_name).as_str(), + true, + Some(data.clone()), + ); + assert!(resp.is_ok()); + + // Get the role field + let resp = test_read_role(c.clone(), token, path, role_name); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["bind_secret_id"].as_bool().unwrap(), data["bind_secret_id"].as_bool().unwrap()); + assert_eq!(resp_data["secret_id_num_uses"].as_i64().unwrap(), data["secret_id_num_uses"].as_i64().unwrap()); + assert_eq!( + resp_data["secret_id_ttl"].as_u64().unwrap(), + data["secret_id_ttl"].as_duration().unwrap().as_secs() + ); + assert_eq!( + resp_data["token_policies"].as_comma_string_slice().unwrap(), + data["token_policies"].as_comma_string_slice().unwrap() + ); + assert_eq!(resp_data["token_ttl"].as_u64().unwrap(), data["token_ttl"].as_duration().unwrap().as_secs()); + assert_eq!( + resp_data["token_max_ttl"].as_u64().unwrap(), + data["token_max_ttl"].as_duration().unwrap().as_secs() + ); + assert_eq!(resp_data["token_num_uses"].as_i64().unwrap(), data["token_num_uses"].as_i64().unwrap()); + assert_eq!(resp_data["token_type"].as_str().unwrap(), data["token_type"].as_str().unwrap()); + + // Get the role-id + let resp = test_read_api(&core, token, format!("auth/{}/role/{}/role-id", path, role_name).as_str(), true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data; + let role_id = resp_data.unwrap()["role_id"].clone(); + let role_id = role_id.as_str().unwrap(); + + // Create a secret-id + let (secret_id, _secret_id_accessor) = generate_secret_id(Arc::clone(&c), token, role_name); + + // Ensure login works + let _ = test_login(Arc::clone(&c), path, role_id, &secret_id, true); + + // Get the role field + let resp = test_read_role(c.clone(), token, path, role_name); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + println!("resp_data: {:?}", resp_data); + } + + fn rusty_vault_init(dir: &str) -> (String, Arc>) { + let root_token; + + let mut conf: HashMap = HashMap::new(); + conf.insert("path".to_string(), Value::String(dir.to_string())); + + let backend = storage::new_backend("file", &conf).unwrap(); + + let barrier = storage::barrier_aes_gcm::AESGCMBarrier::new(Arc::clone(&backend)); + + let c = Arc::new(RwLock::new(Core { physical: backend, barrier: Arc::new(barrier), ..Default::default() })); + + { + let mut core = c.write().unwrap(); + assert!(core.config(Arc::clone(&c), None).is_ok()); + + let seal_config = SealConfig { secret_shares: 10, secret_threshold: 5 }; + + let result = core.init(&seal_config); + assert!(result.is_ok()); + let init_result = result.unwrap(); + println!("init_result: {:?}", init_result); + + let mut unsealed = false; + for i in 0..seal_config.secret_threshold { + let key = &init_result.secret_shares[i as usize]; + let unseal = core.unseal(key); + assert!(unseal.is_ok()); + unsealed = unseal.unwrap(); + } + + root_token = init_result.root_token; + println!("root_token: {:?}", root_token); + + assert!(unsealed); + } + + (root_token, c) + } + + #[test] + fn test_approle_module() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_module"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + + // Mount approle auth to path: auth/approle + test_mount_approle_auth(core.clone(), &root_token, "approle"); + + test_approle(core.clone(), &root_token, "approle", "samplerolename"); + test_approle(core.clone(), &root_token, "approle", "SAMPLEROLENAME"); + test_approle(core.clone(), &root_token, "approle", "SampleRoleName"); + + test_approle_role_service(core.clone(), &root_token, "approle", "testrole"); + } +} diff --git a/src/modules/credential/approle/path_login.rs b/src/modules/credential/approle/path_login.rs new file mode 100644 index 0000000..985ddc4 --- /dev/null +++ b/src/modules/credential/approle/path_login.rs @@ -0,0 +1,224 @@ +use std::{collections::HashMap, mem, sync::Arc, time::SystemTime}; + +use super::{ + path_role::RoleEntry, + validation::{create_hmac, verify_cidr_role_secret_id_subset}, + AppRoleBackend, AppRoleBackendInner, +}; +use crate::{ + context::Context, + errors::RvError, + logical::{Auth, Backend, Field, FieldType, Operation, Path, PathOperation, Request, Response}, + new_fields, new_fields_internal, new_path, new_path_internal, + storage::StorageEntry, + utils::cidr, +}; + +impl AppRoleBackend { + pub fn login_path(&self) -> Path { + let approle_backend_ref = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"login$", + fields: { + "role_id": { + field_type: FieldType::Str, + required: true, + description: "Unique identifier of the Role. Required to be supplied when the 'bind_secret_id' constraint is set." + }, + "secret_id": { + field_type: FieldType::Str, + required: true, + description: "SecretID belong to the App role" + } + }, + operations: [ + {op: Operation::Write, handler: approle_backend_ref.login} + ], + help: r#" +While the credential 'role_id' is required at all times, +other credentials required depends on the properties App role +to which the 'role_id' belongs to. The 'bind_secret_id' +constraint (enabled by default) on the App role requires the +'secret_id' credential to be presented. + +'role_id' is fetched using the 'role//role_id' +endpoint and 'secret_id' is fetched using the 'role//secret_id' +endpoint."# + }); + + path + } +} + +impl AppRoleBackendInner { + pub fn login(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let role_id = req.get_data_as_str("role_id")?; + + let role_id_entry = self.get_role_id(req, &role_id)?; + if role_id_entry.is_none() { + return Err(RvError::ErrResponse("invalid role_id".to_string())); + } + + let role_id_entry = role_id_entry.unwrap(); + let role_name = role_id_entry.name.clone(); + + let role_entry: RoleEntry; + { + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.read()?; + + role_entry = self + .get_role(req, &role_id_entry.name)? + .ok_or_else(|| RvError::ErrResponse("invalid role_id".to_string()))?; + } + + let mut metadata: HashMap = HashMap::new(); + + let storage = Arc::as_ref(req.storage.as_ref().unwrap()); + + if role_entry.bind_secret_id { + let secret_id = req.get_data_as_str("secret_id")?; + + let secret_id_hmac = create_hmac(&role_entry.hmac_key, &secret_id)?; + let role_name_hmac = create_hmac(&role_entry.hmac_key, &role_entry.name)?; + + let entry_index = format!("{}{}/{}", &role_entry.secret_id_prefix, &role_name_hmac, &secret_id_hmac); + + let lock_entry = self.secret_id_locks.get_lock(&secret_id_hmac); + let locked = lock_entry.lock.read()?; + + let secret_id_entry = self + .get_secret_id_storage_entry(storage, &role_entry.secret_id_prefix, &role_name_hmac, &secret_id_hmac)? + .ok_or(RvError::ErrResponse("invalid secret id".to_string()))?; + + // If a secret ID entry does not have a corresponding accessor entry, revoke the secret ID immediately + let accessor_entry = self.get_secret_id_accessor_entry( + storage, + &secret_id_entry.secret_id_accessor, + &role_entry.secret_id_prefix, + )?; + if accessor_entry.is_none() { + if let Err(err) = storage.delete(&entry_index) { + return Err(RvError::ErrResponse(format!( + "error deleting secret_id {} from storage: {}", + &secret_id_hmac, err + ))); + } + + return Err(RvError::ErrResponse("invalid secret_id".to_string())); + } + + if secret_id_entry.secret_id_num_uses == 0 { + // secret_id_num_uses will be zero only if the usage limit was not set at all, in which case, + // the secret_id will remain to be valid as long as it is not expired. + + // Ensure that the CIDRs on the secret id are still a subset of that of role's + verify_cidr_role_secret_id_subset(&secret_id_entry.cidr_list, &role_entry.secret_id_bound_cidrs)?; + + if !secret_id_entry.cidr_list.is_empty() { + let conn = req + .connection + .as_ref() + .ok_or_else(|| RvError::ErrResponse("failed to get connection information".to_string()))?; + if conn.peer_addr.is_empty() { + return Err(RvError::ErrResponse("failed to get connection information".to_string())); + } + + let cidr_list_ref: Vec<&str> = secret_id_entry.cidr_list.iter().map(AsRef::as_ref).collect(); + if !cidr::ip_belongs_to_cidrs(&conn.peer_addr, &cidr_list_ref)? { + return Err(RvError::ErrResponse(format!( + "source address {} unauthorized through CIDR restrictions on the secret ID", + conn.peer_addr + ))); + } + } + } else { + // If the secret_id_num_uses is non-zero, it means that its use-count should be updated in the storage. + // Switch the lock from a `read` to a `write` and update the storage entry. + mem::drop(locked); + let _locked = lock_entry.lock.write()?; + + // Lock switching may change the data. Refresh the contents. + let mut secret_id_entry = self + .get_secret_id_storage_entry( + storage, + &role_entry.secret_id_prefix, + &role_name_hmac, + &secret_id_hmac, + )? + .ok_or(RvError::ErrResponse("invalid secret id".to_string()))?; + + // If there exists a single use left, delete the secret_id entry from the storage but do not fail the + // validation request. Subsequent requests to use the same secret_id will fail. + if secret_id_entry.secret_id_num_uses == 1 { + // Delete the secret IDs accessor first + self.delete_secret_id_accessor_entry( + storage, + &secret_id_entry.secret_id_accessor, + &role_entry.secret_id_prefix, + )?; + + storage.delete(&entry_index)?; + } else { + secret_id_entry.secret_id_num_uses -= 1; + secret_id_entry.last_updated_time = SystemTime::now(); + let entry = StorageEntry::new(&entry_index, &secret_id_entry)?; + storage.put(&entry)?; + } + + // Ensure that the CIDRs on the secret ID are still a subset of that of role's + verify_cidr_role_secret_id_subset(&secret_id_entry.cidr_list, &role_entry.secret_id_bound_cidrs)?; + + if !secret_id_entry.cidr_list.is_empty() { + let conn = req + .connection + .as_ref() + .ok_or_else(|| RvError::ErrResponse("failed to get connection information".to_string()))?; + if conn.peer_addr.is_empty() { + return Err(RvError::ErrResponse("failed to get connection information".to_string())); + } + + let cidr_list_ref: Vec<&str> = secret_id_entry.cidr_list.iter().map(AsRef::as_ref).collect(); + if !cidr::ip_belongs_to_cidrs(&conn.peer_addr, &cidr_list_ref)? { + return Err(RvError::ErrResponse(format!( + "source address {} unauthorized through CIDR restrictions on the secret ID", + conn.peer_addr + ))); + } + } + } + + metadata = secret_id_entry.metadata; + } + + if !role_entry.secret_id_bound_cidrs.is_empty() { + let conn = req + .connection + .as_ref() + .ok_or_else(|| RvError::ErrResponse("failed to get connection information".to_string()))?; + if conn.peer_addr.is_empty() { + return Err(RvError::ErrResponse("failed to get connection information".to_string())); + } + + let bound_cidrs_ref: Vec<&str> = role_entry.secret_id_bound_cidrs.iter().map(AsRef::as_ref).collect(); + if !cidr::ip_belongs_to_cidrs(&conn.peer_addr, &bound_cidrs_ref)? { + return Err(RvError::ErrResponse(format!( + "source address {} unauthorized by CIDR restrictions on the secret ID", + conn.peer_addr + ))); + } + } + + metadata.insert("role_name".to_string(), role_entry.name.clone()); + + let mut auth = Auth { metadata, ..Default::default() }; + auth.internal_data.insert("role_name".to_string(), role_entry.name.clone()); + + role_entry.populate_token_auth(&mut auth); + + let resp = Response { auth: Some(auth), ..Response::default() }; + + Ok(Some(resp)) + } +} diff --git a/src/modules/credential/approle/path_role.rs b/src/modules/credential/approle/path_role.rs new file mode 100644 index 0000000..f73a971 --- /dev/null +++ b/src/modules/credential/approle/path_role.rs @@ -0,0 +1,4443 @@ +use std::{collections::HashMap, mem, sync::Arc, time::Duration}; + +use derive_more::{Deref, DerefMut}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +use super::{ + validation::{create_hmac, verify_cidr_role_secret_id_subset, SecretIdStorageEntry}, + AppRoleBackend, AppRoleBackendInner, HMAC_INPUT_LEN_MAX, SECRET_ID_LOCAL_PREFIX, SECRET_ID_PREFIX, +}; +use crate::{ + context::Context, + errors::RvError, + logical::{field::FieldTrait, Backend, Field, FieldType, Operation, Path, PathOperation, Request, Response}, + new_fields, new_fields_internal, new_path, new_path_internal, + storage::StorageEntry, + utils::{ + self, deserialize_duration, + policy::sanitize_policies, + serialize_duration, + sock_addr::SockAddrMarshaler, + token_util::{token_fields, TokenParams}, + }, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, Deref, DerefMut)] +pub struct RoleEntry { + // Name of the role. This field is not persisted on disk. After the role is read out of disk, + // the sanitized version of name is set in this field for subsequent use of role name + // elsewhere. + pub name: String, + + // UUID that uniquely represents this role. This serves as a credential to perform login using + // this role. + pub role_id: String, + + // UUID that serves as the HMAC key for the hashing the 'secret_id's of the role + pub hmac_key: String, + + // Policies that are to be required by the token to access this role. Deprecated. + pub policies: Vec, + + // lower_case_role_name enforces the lower casing of role names for all the + pub lower_case_role_name: bool, + + // A constraint, if set, requires 'secret_id' credential to be presented during login + pub bind_secret_id: bool, + + // Number of times the secret_id generated against this role can be used to perform login + // operation + pub secret_id_num_uses: i64, + + // SecretIDPrefix is the storage prefix for persisting secret IDs. This differs based on + // whether the secret IDs are cluster local or not. + pub secret_id_prefix: String, + + // Deprecated: A constraint, if set, specifies the CIDR blocks from which logins should be + // allowed, please use secret_id_bound_cidrs instead. + #[serde(rename = "bound_cidr_list", default)] + pub bound_cidr_list_old: String, + + // Deprecated: A constraint, if set, specifies the CIDR blocks from which logins should be + // allowed, please use secret_id_bound_cidrs instead. + #[serde(rename = "bound_cidr_list_list", skip_serializing_if = "Vec::is_empty", default)] + pub bound_cidr_list: Vec, + + // A constraint, if set, specifies the CIDR blocks from which logins should be allowed + pub secret_id_bound_cidrs: Vec, + + // Duration (less than the backend mount's max TTL) after which a secret_id generated against + // the role will expire + #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] + pub secret_id_ttl: Duration, + #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] + // Period, if set, indicates that the token generated using this role should never expire. The + // token should be renewed within the duration specified by this value. The renewal duration + // will be fixed if the value is not modified on the role. If the `Period` in the role is + // modified, a token will pick up the new value during its next renewal. Deprecated. + pub period: Duration, + #[serde(flatten)] + #[deref] + #[deref_mut] + pub token_params: TokenParams, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RoleIdEntry { + pub name: String, +} + +impl Default for RoleEntry { + fn default() -> Self { + Self { + name: String::new(), + role_id: String::new(), + hmac_key: String::new(), + policies: Vec::new(), + lower_case_role_name: true, + bind_secret_id: false, + secret_id_num_uses: 0, + secret_id_prefix: String::new(), + bound_cidr_list_old: String::new(), + bound_cidr_list: Vec::new(), + secret_id_bound_cidrs: Vec::new(), + secret_id_ttl: Duration::from_secs(0), + period: Duration::from_secs(0), + token_params: TokenParams::default(), + } + } +} + +impl Default for RoleIdEntry { + fn default() -> Self { + Self { name: String::new() } + } +} + +impl RoleEntry { + pub fn validate_role_constraints(&self) -> Result<(), RvError> { + if self.bind_secret_id + || self.bound_cidr_list.len() > 0 + || self.secret_id_bound_cidrs.len() > 0 + || self.token_bound_cidrs.len() > 0 + { + return Ok(()); + } + + Err(RvError::ErrResponse("at least one constraint should be enabled on the role".to_string())) + } +} + +impl AppRoleBackend { + // role_path creates all the paths that are used to register and manage a role. + // + // role/ - For listing all the registered roles + pub fn role_path(&self) -> Path { + let approle_backend_ref = Arc::clone(&self.inner); + + let mut path = new_path!({ + pattern: r"role/?", + operations: [ + {op: Operation::List, handler: approle_backend_ref.list_role} + ], + help: "Lists all the roles registered with the backend." + }); + + path.fields.extend(token_fields()); + + path + } + + // role/ - For registering a role + pub fn role_name_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let mut path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "bind_secret_id": { + field_type: FieldType::Bool, + default: true, + description: "Impose secret_id to be presented when logging in using this role. Defaults to 'true'." + }, + "bound_cidr_list": { + field_type: FieldType::CommaStringSlice, + required: false, + description: r#"Use "secret_id_bound_cidrs" instead."# + }, + "secret_id_bound_cidrs": { + field_type: FieldType::CommaStringSlice, + required: false, + description: r#"Comma separated string or list of CIDR blocks. + If set, specifies the blocks of IP addresses which can perform the login operation."# + }, + "secret_id_num_uses": { + field_type: FieldType::Int, + required: false, + description: r#"Number of times a SecretID can access the role, after which the SecretID + will expire. Defaults to 0 meaning that the the secret_id is of unlimited use."# + }, + "secret_id_ttl": { + field_type: FieldType::DurationSecond, + required: false, + description: r#"Duration in seconds after which the issued SecretID should expire. Defaults to 0, meaning no expiration."# + }, + "policies": { + field_type: FieldType::CommaStringSlice, + required: false, + description: "Use token_policies instead. If this and token_policies are both speicified, only token_policies will be used." + }, + "period": { + field_type: FieldType::DurationSecond, + default: 0, + description: "Use token_period instead. If this and token_period are both speicified, only token_period will be used." + }, + "role_id": { + field_type: FieldType::Str, + description: "Identifier of the role. Defaults to a UUID." + }, + "local_secret_ids": { + field_type: FieldType::Bool, + default: false, + description: "If set, the secret IDs generated using this role will be cluster local. This can only be set during role creation and once set, it can't be reset later." + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role}, + {op: Operation::Write, handler: approle_backend_ref2.write_role}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role} + ], + help: r#" +A role can represent a service, a machine or anything that can be IDed. +The set of policies on the role defines access to the role, meaning, any +Vault token with a policy set that is a superset of the policies on the +role registered here will have access to the role. If a SecretID is desired +to be generated against only this specific role, it can be done via +'role//secret-id' and 'role//custom-secret-id' endpoints. +The properties of the SecretID created against the role and the properties +of the token issued with the SecretID generated against the role, can be +configured using the fields of this endpoint. + "# + }); + + path.fields.extend(token_fields()); + + path + } + + // role//policies - For updating the param + pub fn role_policies_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/policies$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "policies": { + field_type: FieldType::CommaStringSlice, + required: false, + description: "Use token_policies instead. If this and token_policies are both speicified, only token_policies will be used." + }, + "token_policies": { + field_type: FieldType::CommaStringSlice, + required: true, + description: "Comma-separated list of policies" + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_policies}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_policies}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role_policies} + ], + help: r#" +A comma-delimited set of Vault policies that defines access to the role. +All the Vault tokens with policies that encompass the policy set +defined on the role, can access the role. + "# + }); + + path + } + + // role//local-secret-ids - For reading the param + pub fn role_local_secret_ids_path(&self) -> Path { + let approle_backend_ref = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/local-secret-ids$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref.read_role_local_secret_ids} + ], + help: r#"If set, the secret IDs generated using this role will be cluster local. +This can only be set during role creation and once set, it can't be reset later. + "# + }); + + path + } + + // role//bound-cidr-list - For updating the param + pub fn role_bound_cidr_list_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/bound-cidr-list$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "bound_cidr_list": { + field_type: FieldType::CommaStringSlice, + description: r#"Comma separated string or list of CIDR blocks. + If set, specifies the blocks of IP addresses which can perform the login operation."# + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_bound_cidr_list}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_bound_cidr_list}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role_bound_cidr_list} + ], + help: r#" +During login, the IP address of the client will be checked to see if it +belongs to the CIDR blocks specified. If CIDR blocks were set and if the +IP is not encompassed by it, login fails + "# + }); + + path + } + + // role//secret-id-bound-cidrs - For updating the param + pub fn role_secret_id_bound_cidrs_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/secret-id-bound-cidrs$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "secret_id_bound_cidrs": { + field_type: FieldType::CommaStringSlice, + description: r#"Comma separated string or list of CIDR blocks. + If set, specifies the blocks of IP addresses which can perform the login operation."# + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_secret_id_bound_cidrs}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_secret_id_bound_cidrs}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role_secret_id_bound_cidrs} + ], + help: r#" +During login, the IP address of the client will be checked to see if it +belongs to the CIDR blocks specified. If CIDR blocks were set and if the +IP is not encompassed by it, login fails + "# + }); + + path + } + + // role//token-bound-cidrs - For updating the param + pub fn role_token_bound_cidrs_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/token-bound-cidrs$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "token_bound_cidrs": { + field_type: FieldType::CommaStringSlice, + description: r#"Comma separated string or list of CIDR blocks. If set, specifies the blocks of IP addresses which can use the returned token. Should be a subset of the token CIDR blocks listed on the role, if any."# + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_token_bound_cidrs}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_token_bound_cidrs}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role_token_bound_cidrs} + ], + help: r#" +During use of the returned token, the IP address of the client will be checked to see if it +belongs to the CIDR blocks specified. If CIDR blocks were set and if the +IP is not encompassed by it, token use fails + "# + }); + + path + } + + // role//bind-secret-id - For updating the param + pub fn role_bind_secret_id_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/bind-secret-id$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "bind_secret_id": { + field_type: FieldType::Bool, + default: true, + description: "Impose secret_id to be presented when logging in using this role. Defaults to 'true'." + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_bind_secret_id}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_bind_secret_id}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role_bind_secret_id} + ], + help: r#" +By setting this to 'true', during login the field 'secret_id' becomes a mandatory argument. +The value of 'secret_id' can be retrieved using 'role//secret-id' endpoint. + "# + }); + + path + } + + // role//secret-id-num-users - For updating the param + pub fn role_secret_id_num_uses_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/secret-id-num-uses$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "secret_id_num_uses": { + field_type: FieldType::Int, + default: 0, + description: "Number of times a secret ID can access the role, after which the SecretID will expire. Defaults to 0 meaning that the secret ID is of unlimited use." + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_secret_id_num_uses}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_secret_id_num_uses}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role_secret_id_num_uses} + ], + help: r#" +If a SecretID is generated/assigned against a role using the +'role//secret-id' or 'role//custom-secret-id' endpoint, +then the number of times this SecretID can be used is defined by this option. +However, this option may be overriden by the request's 'num_uses' field. + "# + }); + + path + } + + // role//secret-id-ttl - For updating the param + pub fn role_secret_id_ttl_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/secret-id-ttl$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "secret_id_ttl": { + field_type: FieldType::Int, + default: 0, + description: "Duration in seconds after which the issued secret ID should expire. Defaults to 0, meaning no expiration." + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_secret_id_ttl}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_secret_id_ttl}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role_secret_id_ttl} + ], + help: r#" +If a SecretID is generated/assigned against a role using the +'role//secret-id' or 'role//custom-secret-id' endpoint, +then the lifetime of this SecretID is defined by this option. +However, this option may be overridden by the request's 'ttl' field. + "# + }); + + path + } + + // role//period - For updating the param + pub fn role_period_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/period$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "period": { + field_type: FieldType::DurationSecond, + default: 0, + description: "Use token_period instead. If this and token_period are both speicified, only token_period will be used." + }, + "token_period": { + field_type: FieldType::DurationSecond, + description: "If set, tokens created via this role will have no max lifetime; instead, their renewal period will be fixed to this value." + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_period}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_period}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role_period} + ], + help: r#" +If set, indicates that the token generated using this role +should never expire. The token should be renewed within the +duration specified by this value. The renewal duration will +be fixed. If the Period in the role is modified, the token +will pick up the new value during its next renewal. + "# + }); + + path + } + + // role//token-num-uses - For updating the param + pub fn role_token_num_uses_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/token-num-uses$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "token_num_uses": { + field_type: FieldType::Int, + default: 0, + description: "The maximum number of times a token may be used, a value of zero means unlimited" + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_token_num_uses}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_token_num_uses}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role_token_num_uses} + ], + help: "By default, this will be set to zero, indicating that the issued" + }); + + path + } + + // role//token-ttl - For updating the param + pub fn role_token_ttl_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/token-ttl$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "token_ttl": { + field_type: FieldType::DurationSecond, + description: "The initial ttl of the token to generate" + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_token_ttl}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_token_ttl}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role_token_ttl} + ], + help: r#" +If SecretIDs are generated against the role, using 'role//secret-id' or the +'role//custom-secret-id' endpoints, and if those SecretIDs are used +to perform the login operation, then the value of 'token-ttl' defines the +lifetime of the token issued, before which the token needs to be renewed. + "# + }); + + path + } + + // role//token-max-ttl - For updating the param + pub fn role_token_max_ttl_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + let approle_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/token-max-ttl$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "token_max_ttl": { + field_type: FieldType::DurationSecond, + description: "The maximum lifetime of the generated token" + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_token_max_ttl}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_token_max_ttl}, + {op: Operation::Delete, handler: approle_backend_ref3.delete_role_token_max_ttl} + ], + help: r#" +If SecretIDs are generated against the role using 'role//secret-id' +or the 'role//custom-secret-id' endpoints, and if those SecretIDs +are used to perform the login operation, then the value of 'token-max-ttl' +defines the maximum lifetime of the tokens issued, after which the tokens +cannot be renewed. A reauthentication is required after this duration. +This value will be capped by the backend mount's maximum TTL value. + "# + }); + + path + } + + // role//role-id - For updating the param + pub fn role_role_id_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/role-id$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "role_id": { + field_type: FieldType::Str, + description: "Identifier of the role. Defaults to a UUID." + } + }, + operations: [ + {op: Operation::Read, handler: approle_backend_ref1.read_role_role_id}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_role_id} + ], + help: r#" +If login is performed from an role, then its 'role_id' should be presented +as a credential during the login. This 'role_id' can be retrieved using +this endpoint."# + }); + + path + } + + // role//secret-id - For issuing a secret_id against a role, also to list the secret_id_accessors + pub fn role_secret_id_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/secret-id/?$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "metadata": { + field_type: FieldType::Str, + description: r#"Metadata to be tied to the SecretID. This should be a JSON + formatted string containing the metadata in key value pairs."# + }, + "cidr_list": { + field_type: FieldType::CommaStringSlice, + description: r#"Comma separated string or list of CIDR blocks enforcing secret IDs to be used from +specific set of IP addresses. If 'bound_cidr_list' is set on the role, then the +list of CIDR blocks listed here should be a subset of the CIDR blocks listed on +the role."# + }, + "token_bound_cidrs": { + field_type: FieldType::CommaStringSlice, + description: r#"List of CIDR blocks. If set, specifies the blocks of IP addresses which can use the returned token. Should be a subset of the token CIDR blocks listed on the role, if any."# + }, + "num_uses": { + field_type: FieldType::Int, + description: r#"Number of times this SecretID can be used, after which the SecretID expires. + Overrides secret_id_num_uses role option when supplied. May not be higher than role's secret_id_num_uses."# + }, + "ttl": { + field_type: FieldType::DurationSecond, + description: r#"Duration in seconds after which this SecretID expires. + Overrides secret_id_ttl role option when supplied. May not be longer than role's secret_id_ttl."# + } + }, + operations: [ + {op: Operation::List, handler: approle_backend_ref1.list_role_secret_id}, + {op: Operation::Write, handler: approle_backend_ref2.write_role_secret_id} + ], + help: r#" +The SecretID generated using this endpoint will be scoped to access +just this role and none else. The properties of this SecretID will be +based on the options set on the role. It will expire after a period +defined by the 'ttl' field or 'secret_id_ttl' option on the role, +and/or the backend mount's maximum TTL value."# + }); + + path + } + + // role//secret-id/lookup - For reading the properties of a secret_id + pub fn role_secret_id_lookup_path(&self) -> Path { + let approle_backend_ref = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/secret-id/lookup/?$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "secret_id": { + field_type: FieldType::Str, + description: "SecretID attached to the role." + } + }, + operations: [ + {op: Operation::Write, handler: approle_backend_ref.write_role_secret_id_lookup} + ], + help: "This endpoint is used to read the properties of a secret_id associated to a role." + }); + + path + } + + // role//secret-id/destroy - For deleting a secret_id + pub fn role_secret_id_destroy_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/secret-id/destroy/?$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "secret_id": { + field_type: FieldType::Str, + description: "SecretID attached to the role." + } + }, + operations: [ + {op: Operation::Write, handler: approle_backend_ref1.write_role_secret_id_destory}, + {op: Operation::Delete, handler: approle_backend_ref2.delete_role_secret_id_destory} + ], + help: "This endpoint is used to delete the properties of a secret_id associated to a role." + }); + + path + } + + // role//secret-id-accessor/lookup - For reading secret_id using accessor + pub fn role_secret_id_accessor_lookup_path(&self) -> Path { + let approle_backend_ref = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/secret-id-accessor/lookup/?$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "secret_id_accessor": { + field_type: FieldType::Str, + description: "Accessor of the SecretID" + } + }, + operations: [ + {op: Operation::Write, handler: approle_backend_ref.write_role_secret_id_accessor_lookup} + ], + help: r#" +This is particularly useful to lookup the non-expiring 'secret_id's. +The list operation on the 'role//secret-id' endpoint will return +the 'secret_id_accessor's. This endpoint can be used to read the properties +of the secret. If the 'secret_id_num_uses' field in the response is 0, it +represents a non-expiring 'secret_id'."# + }); + + path + } + + // role//secret-id-accessor/destroy - For deleting secret_id using accessor + pub fn role_secret_id_accessor_destroy_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/secret-id-accessor/destroy/?$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "secret_id_accessor": { + field_type: FieldType::Str, + description: "Accessor of the SecretID" + } + }, + operations: [ + {op: Operation::Write, handler: approle_backend_ref1.write_role_secret_id_accessor_destory}, + {op: Operation::Delete, handler: approle_backend_ref2.delete_role_secret_id_accessor_destory} + ], + help: r#" +This is particularly useful to clean-up the non-expiring 'secret_id's. +The list operation on the 'role//secret-id' endpoint will return +the 'secret_id_accessor's. This endpoint can be used to read the properties +of the secret. If the 'secret_id_num_uses' field in the response is 0, it +represents a non-expiring 'secret_id'."# + }); + + path + } + + // role//custom-secret-id - For assigning a custom SecretID against a role + pub fn role_custom_secret_id_path(&self) -> Path { + let approle_backend_ref = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"role/(?P\w[\w-]+\w)/custom-secret-id$", + fields: { + "role_name": { + field_type: FieldType::Str, + required: true, + description: "Name of the role." + }, + "secret_id": { + field_type: FieldType::Str, + description: "SecretID to be attached to the role." + }, + "metadata": { + field_type: FieldType::Str, + description: r#"Metadata to be tied to the SecretID. This should be a JSON + formatted string containing the metadata in key value pairs."# + }, + "cidr_list": { + field_type: FieldType::CommaStringSlice, + description: r#"Comma separated string or list of CIDR blocks enforcing secret IDs to be used from +specific set of IP addresses. If 'bound_cidr_list' is set on the role, then the +list of CIDR blocks listed here should be a subset of the CIDR blocks listed on +the role."# + }, + "token_bound_cidrs": { + field_type: FieldType::CommaStringSlice, + description: r#"List of CIDR blocks. If set, specifies the blocks of IP addresses which can use the returned token. Should be a subset of the token CIDR blocks listed on the role, if any."# + }, + "num_uses": { + field_type: FieldType::Int, + description: r#"Number of times this SecretID can be used, after which the SecretID expires. + Overrides secret_id_num_uses role option when supplied. May not be higher than role's secret_id_num_uses."# + }, + "ttl": { + field_type: FieldType::DurationSecond, + description: r#"Duration in seconds after which this SecretID expires. + Overrides secret_id_ttl role option when supplied. May not be longer than role's secret_id_ttl."# + } + }, + operations: [ + {op: Operation::Write, handler: approle_backend_ref.write_role_custom_secret_id} + ], + help: r#" +This option is not recommended unless there is a specific need +to do so. This will assign a client supplied SecretID to be used to access +the role. This SecretID will behave similarly to the SecretIDs generated by +the backend. The properties of this SecretID will be based on the options +set on the role. It will expire after a period defined by the 'ttl' field +or 'secret_id_ttl' option on the role, and/or the backend mount's maximum TTL value."# + }); + + path + } + + pub fn role_paths(&self) -> Vec { + let mut paths: Vec = Vec::with_capacity(21); + paths.push(self.role_path()); + paths.push(self.role_name_path()); + paths.push(self.role_policies_path()); + paths.push(self.role_local_secret_ids_path()); + paths.push(self.role_bound_cidr_list_path()); + paths.push(self.role_secret_id_bound_cidrs_path()); + paths.push(self.role_token_bound_cidrs_path()); + paths.push(self.role_bind_secret_id_path()); + paths.push(self.role_secret_id_num_uses_path()); + paths.push(self.role_secret_id_ttl_path()); + paths.push(self.role_period_path()); + paths.push(self.role_token_num_uses_path()); + paths.push(self.role_token_ttl_path()); + paths.push(self.role_token_max_ttl_path()); + paths.push(self.role_role_id_path()); + paths.push(self.role_secret_id_path()); + paths.push(self.role_secret_id_lookup_path()); + paths.push(self.role_secret_id_destroy_path()); + paths.push(self.role_secret_id_accessor_lookup_path()); + paths.push(self.role_secret_id_accessor_destroy_path()); + paths.push(self.role_custom_secret_id_path()); + paths + } +} + +impl AppRoleBackendInner { + pub fn get_role_id(&self, req: &mut Request, role_id: &str) -> Result, RvError> { + if role_id == "" { + return Err(RvError::ErrResponse("missing role_id".to_string())); + } + + let salt = self.salt.read()?; + if salt.is_none() { + return Err(RvError::ErrResponse("salt not found".to_string())); + } + + let salt_id = salt.as_ref().unwrap().salt_id(role_id)?; + let storage_entry = req.storage_get(format!("role_id/{}", salt_id).as_str())?; + if storage_entry.is_none() { + return Ok(None); + } + + let entry = storage_entry.unwrap(); + let role_id_entry: RoleIdEntry = serde_json::from_slice(entry.value.as_slice())?; + + Ok(Some(role_id_entry)) + } + + pub fn set_role_id(&self, req: &mut Request, role_id: &str, role_id_entry: &RoleIdEntry) -> Result<(), RvError> { + let salt = self.salt.read()?; + if salt.is_none() { + return Err(RvError::ErrResponse("salt not found".to_string())); + } + + let salt_id = salt.as_ref().unwrap().salt_id(role_id)?; + + let entry = StorageEntry::new(format!("role_id/{}", salt_id).as_str(), role_id_entry)?; + + req.storage_put(&entry) + } + + pub fn delete_role_id(&self, req: &mut Request, role_id: &str) -> Result<(), RvError> { + if role_id == "" { + return Err(RvError::ErrResponse("missing role_id".to_string())); + } + + let salt = self.salt.read()?; + if salt.is_none() { + return Err(RvError::ErrResponse("salt not found".to_string())); + } + + let salt_id = salt.as_ref().unwrap().salt_id(role_id)?; + + req.storage_delete(format!("role_id/{}", salt_id).as_str())?; + + Ok(()) + } + + pub fn get_role(&self, req: &mut Request, name: &str) -> Result, RvError> { + let key = format!("role/{}", name.to_lowercase()); + let storage_entry = req.storage_get(&key)?; + if storage_entry.is_none() { + return Ok(None); + } + + let entry = storage_entry.unwrap(); + let mut role_entry: RoleEntry = serde_json::from_slice(entry.value.as_slice())?; + + role_entry.name = name.to_string(); + if role_entry.lower_case_role_name { + role_entry.name = name.to_lowercase(); + } + + if role_entry.secret_id_prefix == "" { + role_entry.secret_id_prefix = SECRET_ID_PREFIX.to_string(); + } + + if role_entry.bound_cidr_list_old != "" { + role_entry.secret_id_bound_cidrs = + role_entry.bound_cidr_list_old.split(',').map(|s| s.to_string()).collect(); + role_entry.bound_cidr_list_old.clear(); + } + + if role_entry.bound_cidr_list.len() != 0 { + role_entry.secret_id_bound_cidrs = role_entry.bound_cidr_list.clone(); + role_entry.bound_cidr_list.clear(); + } + + if role_entry.token_period.as_secs() == 0 && role_entry.period.as_secs() > 0 { + role_entry.token_period = role_entry.period; + } + + if role_entry.token_policies.len() == 0 && role_entry.policies.len() > 0 { + role_entry.token_policies = role_entry.policies.clone(); + } + + Ok(Some(role_entry)) + } + + pub fn set_role( + &self, + req: &mut Request, + name: &str, + role_entry: &RoleEntry, + previous_role_id: &str, + ) -> Result<(), RvError> { + if name == "" { + return Err(RvError::ErrResponse("missing role name".to_string())); + } + + role_entry.validate_role_constraints()?; + + if let Some(role_id_entry) = self.get_role_id(req, &role_entry.role_id)? { + if role_id_entry.name.as_str() != name { + return Err(RvError::ErrResponse("role_id already in use".to_string())); + } + } + + let mut create_role_id = true; + + if previous_role_id != "" { + if previous_role_id != role_entry.role_id.as_str() { + self.delete_role_id(req, previous_role_id)?; + } else { + create_role_id = false; + } + } + + let entry = StorageEntry::new(format!("role/{}", name.to_lowercase()).as_str(), role_entry)?; + + req.storage_put(&entry)?; + + if create_role_id { + return self.set_role_id(req, &role_entry.role_id, &RoleIdEntry { name: name.to_string() }); + } + + Ok(()) + } + + pub fn list_role(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let roles = req.storage_list("role/")?; + Ok(Some(Response::list_response(&roles))) + } + + pub fn write_role(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let role_name_value = req.get_data("role_name")?; + let role_name = role_name_value.as_str().ok_or(RvError::ErrRequestFieldInvalid)?; + + if role_name.len() > HMAC_INPUT_LEN_MAX { + return Err(RvError::ErrResponse( + format!("role_name is longer than maximum of {} bytes", HMAC_INPUT_LEN_MAX).to_string(), + )); + } + + let mut role_entry = RoleEntry::default(); + let mut create = false; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.write()?; + + let entry = self.get_role(req, role_name)?; + if entry.is_some() { + role_entry = entry.unwrap(); + } else { + role_entry.name = role_name.to_lowercase(); + role_entry.lower_case_role_name = true; + role_entry.hmac_key = utils::generate_uuid(); + create = true; + } + + let old_token_policies = role_entry.token_policies.clone(); + let old_token_period = role_entry.token_period.clone(); + + role_entry.parse_token_fields(req)?; + + if old_token_policies != role_entry.token_policies { + role_entry.policies = role_entry.token_policies.clone(); + } else if let Ok(policies_value) = req.get_data("policies") { + let policies = policies_value.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + role_entry.policies = policies.clone(); + role_entry.token_policies = policies; + } + + if old_token_period != role_entry.token_period { + role_entry.period = role_entry.token_period.clone(); + } else if let Ok(period_value) = req.get_data("period") { + let period = period_value.as_duration().ok_or(RvError::ErrRequestFieldInvalid)?; + role_entry.period = period.clone(); + role_entry.token_period = period; + } + + if let Ok(local_secret_ids_value) = req.get_data("local_secret_ids") { + let local_secret_ids = local_secret_ids_value.as_bool().ok_or(RvError::ErrRequestFieldInvalid)?; + if local_secret_ids { + if !create { + return Err(RvError::ErrResponse( + "local_secret_ids can only be modified during role creation".to_string(), + )); + } + role_entry.secret_id_prefix = SECRET_ID_LOCAL_PREFIX.to_string(); + } + } + + let previous_role_id = role_entry.role_id.clone(); + + if let Ok(role_id_value) = req.get_data("role_id") { + role_entry.role_id = role_id_value.as_str().ok_or(RvError::ErrRequestFieldInvalid)?.to_string(); + } else if create { + role_entry.role_id = utils::generate_uuid(); + } + + if role_entry.role_id == "" { + return Err(RvError::ErrResponse("invalid role_id supplied, or failed to generate a role_id".to_string())); + } + + if let Ok(bind_secret_id_value) = req.get_data("bind_secret_id") { + role_entry.bind_secret_id = bind_secret_id_value.as_bool().ok_or(RvError::ErrRequestFieldInvalid)?; + } else if create { + role_entry.bind_secret_id = + req.get_data_or_default("bind_secret_id")?.as_bool().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(bound_cidr_list_value) = req.get_data_or_next(&["secret_id_bound_cidrs", "bound_cidr_list"]) { + role_entry.secret_id_bound_cidrs = + bound_cidr_list_value.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if role_entry.secret_id_bound_cidrs.len() != 0 { + let cidrs: Vec<&str> = role_entry.secret_id_bound_cidrs.iter().map(AsRef::as_ref).collect(); + if !utils::cidr::validate_cidrs(&cidrs)? { + return Err(RvError::ErrResponse("invalid CIDR blocks".to_string())); + } + } + + if let Ok(secret_id_num_uses_value) = req.get_data("secret_id_num_uses") { + role_entry.secret_id_num_uses = secret_id_num_uses_value.as_int().ok_or(RvError::ErrRequestFieldInvalid)?; + } else if create { + role_entry.secret_id_num_uses = + req.get_data_or_default("secret_id_num_uses")?.as_int().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if role_entry.secret_id_num_uses < 0 { + return Err(RvError::ErrResponse("secret_id_num_uses cannot be negative".to_string())); + } + + if let Ok(secret_id_ttl_value) = req.get_data("secret_id_ttl") { + role_entry.secret_id_ttl = secret_id_ttl_value.as_duration().ok_or(RvError::ErrRequestFieldInvalid)?; + } else if create { + role_entry.secret_id_ttl = + req.get_data_or_default("secret_id_ttl")?.as_duration().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + self.set_role(req, &role_entry.name, &role_entry, &previous_role_id)?; + + Ok(None) + } + + pub fn read_role(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let locked = lock_entry.lock.read()?; + + if let Some(entry) = self.get_role(req, &role_name)? { + let mut data = serde_json::json!({ + "bind_secret_id": entry.bind_secret_id, + "secret_id_bound_cidrs": entry.secret_id_bound_cidrs, + "secret_id_num_uses": entry.secret_id_num_uses, + "secret_id_ttl": entry.secret_id_ttl.as_secs(), + "local_secret_ids": false, + }) + .as_object() + .unwrap() + .clone(); + + if entry.secret_id_prefix.as_str() == SECRET_ID_LOCAL_PREFIX { + data["local_secret_ids"] = Value::from(true); + } + + if entry.period.as_secs() != 0 { + data.insert("period".to_string(), Value::from(entry.period.as_secs())); + } + + if entry.policies.len() > 0 { + data.insert("policies".to_string(), Value::from(entry.policies.clone())); + } + + entry.populate_token_data(&mut data); + + if entry.validate_role_constraints().is_err() { + log::warn!( + "Role does not have any constraints set on it. Updates to this role will require a constraint to \ + be set" + ); + } + + let mut resp = Response::data_response(Some(data)); + + // For sanity, verify that the index still exists. If the index is missing, + // add one and return a warning so it can be reported. + if self.get_role_id(req, &entry.role_id)?.is_none() { + // Switch to a write lock + mem::drop(locked); + let _locked = lock_entry.lock.write()?; + + // Check again if the index is missing + if self.get_role_id(req, &entry.role_id)?.is_none() { + // Create a new inde + self.set_role_id(req, &entry.role_id, &RoleIdEntry { name: entry.name.clone() })?; + resp.add_warning("Role identifier was missing an index back to role name"); + } + } + + return Ok(Some(resp)); + } + + Ok(None) + } + + pub fn delete_role(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.write()?; + + if let Some(entry) = self.get_role(req, &role_name)? { + let storage = req.storage.as_ref().unwrap(); + + self.flush_role_secrets(Arc::as_ref(storage), &entry.name, &entry.hmac_key, &entry.secret_id_prefix)?; + + self.delete_role_id(req, &entry.role_id)?; + + req.storage_delete(format!("role/{}", role_name.to_lowercase()).as_str())?; + } + + Ok(None) + } + + pub fn read_role_policies(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.read()?; + + if let Some(role) = self.get_role(req, &role_name)? { + let mut data = serde_json::json!({ + "token_policies": role.token_policies, + }) + .as_object() + .unwrap() + .clone(); + + if role.policies.len() > 0 { + data.insert("policies".to_string(), Value::from(role.policies)); + } + + return Ok(Some(Response::data_response(Some(data)))); + } else { + return Ok(None); + } + } + + pub fn write_role_policies(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + + let token_policies_value = req.get_data_or_next(&["token_policies", "policies"])?; + let mut token_policies = token_policies_value.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.write()?; + + if let Some(mut role) = self.get_role(req, &role_name)? { + sanitize_policies(&mut token_policies, false); + role.policies = token_policies.clone(); + role.token_policies = token_policies; + self.set_role(req, &role_name, &role, "")?; + } else { + return Err(RvError::ErrLogicalPathUnsupported); + } + + Ok(None) + } + + pub fn delete_role_policies(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.write()?; + + if let Some(mut role) = self.get_role(req, &role_name)? { + role.token_policies.clear(); + role.policies.clear(); + self.set_role(req, &role_name, &role, "")?; + } else { + return Err(RvError::ErrLogicalPathUnsupported); + } + + Ok(None) + } + + pub fn read_role_local_secret_ids( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.read_role_field(req, "local_secret_ids") + } + + pub fn read_role_field(&self, req: &mut Request, field: &str) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.read()?; + + if let Some(role) = self.get_role(req, &role_name)? { + let data = match field { + "bound_cidr_list" => { + serde_json::json!({ + "bound_cidr_list": role.bound_cidr_list, + }) + } + "secret_id_bound_cidrs" => { + serde_json::json!({ + "secret_id_bound_cidrs": role.secret_id_bound_cidrs, + }) + } + "token_bound_cidrs" => { + serde_json::json!({ + "token_bound_cidrs": role.token_bound_cidrs, + }) + } + "bind_secret_id" => { + serde_json::json!({ + "bind_secret_id": role.bind_secret_id, + }) + } + "local_secret_ids" => { + serde_json::json!({ + "local_secret_ids": role.secret_id_prefix.as_str() == SECRET_ID_LOCAL_PREFIX, + }) + } + "secret_id_num_uses" => { + serde_json::json!({ + "secret_id_num_uses": role.secret_id_num_uses, + }) + } + "role_id" => { + serde_json::json!({ + "role_id": role.role_id, + }) + } + "secret_id_ttl" => { + serde_json::json!({ + "secret_id_ttl": role.secret_id_ttl.as_secs(), + }) + } + "token_period" | "period" => { + if role.period.as_secs() > 0 { + serde_json::json!({ + "token_period": role.token_period.as_secs(), + "period": role.period.as_secs(), + }) + } else { + serde_json::json!({ + "token_period": role.token_period.as_secs(), + }) + } + } + "token_num_uses" => { + serde_json::json!({ + "token_num_uses": role.token_num_uses, + }) + } + "token_ttl" => { + serde_json::json!({ + "token_ttl": role.token_ttl.as_secs(), + }) + } + "token_max_ttl" => { + serde_json::json!({ + "token_max_ttl": role.token_max_ttl.as_secs(), + }) + } + _ => { + return Err(RvError::ErrResponse("unrecognized field".to_string())); + } + }; + return Ok(Some(Response::data_response(Some(data.as_object().unwrap().clone())))); + } else { + return Ok(None); + } + } + + pub fn update_role_field(&self, req: &mut Request, field: &str) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + + let field_value = match field { + "token_period" | "period" => req.get_data_or_next(&["token_period", "period"])?, + _ => req.get_data(field)?, + }; + + let mut cidr_list = Vec::new(); + + match field { + "bound_cidr_list" | "secret_id_bound_cidrs" | "token_bound_cidrs" => { + cidr_list = field_value.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + if cidr_list.len() == 0 { + return Err(RvError::ErrResponse(format!("missing {}", field).to_string())); + } + + let cidrs: Vec<&str> = cidr_list.iter().map(AsRef::as_ref).collect(); + if !utils::cidr::validate_cidrs(&cidrs)? { + return Err(RvError::ErrResponse("failed to validate CIDR blocks".to_string())); + } + } + _ => {} + } + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.write()?; + + let mut previous_role_id = "".to_string(); + + if let Some(mut role) = self.get_role(req, &role_name)? { + match field { + "bound_cidr_list" | "secret_id_bound_cidrs" => { + role.secret_id_bound_cidrs = cidr_list; + } + "token_bound_cidrs" => { + role.token_bound_cidrs = cidr_list + .iter() + .map(|s| SockAddrMarshaler::from_str(s)) + .collect::, _>>()?; + } + "bind_secret_id" => { + role.bind_secret_id = field_value.as_bool().ok_or(RvError::ErrLogicalOperationUnsupported)?; + } + "secret_id_num_uses" => { + role.secret_id_num_uses = field_value.as_int().ok_or(RvError::ErrLogicalOperationUnsupported)?; + if role.secret_id_num_uses < 0 { + return Err(RvError::ErrResponse("secret_id_num_uses cannot be negative".to_string())); + } + } + "role_id" => { + previous_role_id = role.role_id.clone(); + role.role_id = field_value.as_str().ok_or(RvError::ErrLogicalOperationUnsupported)?.to_string(); + if role.role_id.as_str() == "" { + return Err(RvError::ErrResponse("missing role_id".to_string())); + } + } + "secret_id_ttl" => { + role.secret_id_ttl = field_value.as_duration().ok_or(RvError::ErrLogicalOperationUnsupported)?; + } + "token_period" | "period" => { + role.token_period = field_value.as_duration().ok_or(RvError::ErrLogicalOperationUnsupported)?; + role.period = role.token_period; + } + "token_num_uses" => { + role.token_num_uses = field_value.as_u64().ok_or(RvError::ErrLogicalOperationUnsupported)?; + } + "token_ttl" => { + role.token_ttl = field_value.as_duration().ok_or(RvError::ErrLogicalOperationUnsupported)?; + if role.token_max_ttl.as_secs() > 0 && role.token_ttl.as_secs() > role.token_max_ttl.as_secs() { + return Err(RvError::ErrResponse( + "token_ttl should not be greater than token_max_ttl".to_string(), + )); + } + } + "token_max_ttl" => { + role.token_max_ttl = field_value.as_duration().ok_or(RvError::ErrLogicalOperationUnsupported)?; + if role.token_max_ttl.as_secs() > 0 && role.token_ttl.as_secs() > role.token_max_ttl.as_secs() { + return Err(RvError::ErrResponse( + "token_max_ttl should not be greater than token_ttl".to_string(), + )); + } + } + _ => { + return Err(RvError::ErrResponse("unrecognized field".to_string())); + } + } + + self.set_role(req, &role_name, &role, &previous_role_id)?; + } else { + return Err(RvError::ErrLogicalPathUnsupported); + } + + Ok(None) + } + + pub fn delete_role_field(&self, req: &mut Request, field: &str) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.write()?; + + if let Some(mut role) = self.get_role(req, &role_name)? { + match field { + "bound_cidr_list" => { + role.bound_cidr_list.clear(); + } + "secret_id_bound_cidrs" => { + role.secret_id_bound_cidrs.clear(); + } + "token_bound_cidrs" => { + role.token_bound_cidrs.clear(); + } + "bind_secret_id" => { + role.bind_secret_id = req + .get_field_default_or_zero("bind_secret_id")? + .as_bool() + .ok_or(RvError::ErrLogicalOperationUnsupported)?; + } + "secret_id_num_uses" => { + role.secret_id_num_uses = req + .get_field_default_or_zero("secret_id_num_uses")? + .as_int() + .ok_or(RvError::ErrLogicalOperationUnsupported)?; + } + "secret_id_ttl" => { + role.secret_id_ttl = req + .get_field_default_or_zero("secret_id_ttl")? + .as_duration() + .ok_or(RvError::ErrLogicalOperationUnsupported)?; + } + "token_period" | "period" => { + role.token_period = Duration::from_secs(0); + role.period = Duration::from_secs(0); + } + "token_num_uses" => { + role.token_num_uses = req + .get_field_default_or_zero("token_num_uses")? + .as_u64() + .ok_or(RvError::ErrLogicalOperationUnsupported)?; + } + "token_ttl" => { + role.token_ttl = req + .get_field_default_or_zero("token_ttl")? + .as_duration() + .ok_or(RvError::ErrLogicalOperationUnsupported)?; + } + "token_max_ttl" => { + role.token_max_ttl = req + .get_field_default_or_zero("token_max_ttl")? + .as_duration() + .ok_or(RvError::ErrLogicalOperationUnsupported)?; + } + _ => { + return Err(RvError::ErrResponse("unrecognized field".to_string())); + } + } + + self.set_role(req, &role_name, &role, "")?; + } + + return Ok(None); + } + + pub fn read_role_bound_cidr_list( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.read_role_field(req, "bound_cidr_list") + } + + pub fn write_role_bound_cidr_list( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.update_role_field(req, "bound_cidr_list") + } + + pub fn delete_role_bound_cidr_list( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.delete_role_field(req, "bound_cidr_list") + } + + pub fn read_role_secret_id_bound_cidrs( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.read_role_field(req, "secret_id_bound_cidrs") + } + + pub fn write_role_secret_id_bound_cidrs( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.update_role_field(req, "secret_id_bound_cidrs") + } + + pub fn delete_role_secret_id_bound_cidrs( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.delete_role_field(req, "secret_id_bound_cidrs") + } + + pub fn read_role_token_bound_cidrs( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.read_role_field(req, "token_bound_cidrs") + } + + pub fn write_role_token_bound_cidrs( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.update_role_field(req, "token_bound_cidrs") + } + + pub fn delete_role_token_bound_cidrs( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.delete_role_field(req, "token_bound_cidrs") + } + + pub fn read_role_bind_secret_id( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.read_role_field(req, "bind_secret_id") + } + + pub fn write_role_bind_secret_id( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.update_role_field(req, "bind_secret_id") + } + + pub fn delete_role_bind_secret_id( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.delete_role_field(req, "bind_secret_id") + } + + pub fn read_role_secret_id_num_uses( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.read_role_field(req, "secret_id_num_uses") + } + + pub fn write_role_secret_id_num_uses( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.update_role_field(req, "secret_id_num_uses") + } + + pub fn delete_role_secret_id_num_uses( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.delete_role_field(req, "secret_id_num_uses") + } + + pub fn read_role_secret_id_ttl( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.read_role_field(req, "secret_id_ttl") + } + + pub fn write_role_secret_id_ttl( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.update_role_field(req, "secret_id_ttl") + } + + pub fn delete_role_secret_id_ttl( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.delete_role_field(req, "secret_id_ttl") + } + + pub fn read_role_period(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + self.read_role_field(req, "token_period") + } + + pub fn write_role_period(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + self.update_role_field(req, "token_period") + } + + pub fn delete_role_period(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + self.delete_role_field(req, "token_period") + } + + pub fn read_role_token_num_uses( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.read_role_field(req, "token_num_uses") + } + + pub fn write_role_token_num_uses( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.update_role_field(req, "token_num_uses") + } + + pub fn delete_role_token_num_uses( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.delete_role_field(req, "token_num_uses") + } + + pub fn read_role_token_ttl(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + self.read_role_field(req, "token_ttl") + } + + pub fn write_role_token_ttl(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + self.update_role_field(req, "token_ttl") + } + + pub fn delete_role_token_ttl( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.delete_role_field(req, "token_ttl") + } + + pub fn read_role_token_max_ttl( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.read_role_field(req, "token_max_ttl") + } + + pub fn write_role_token_max_ttl( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.update_role_field(req, "token_max_ttl") + } + + pub fn delete_role_token_max_ttl( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.delete_role_field(req, "token_max_ttl") + } + + pub fn read_role_role_id(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + self.read_role_field(req, "role_id") + } + + pub fn write_role_role_id(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + self.update_role_field(req, "role_id") + } + + pub fn list_role_secret_id(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.read()?; + + if let Some(role) = self.get_role(req, &role_name)? { + let role_name_hmac = create_hmac(&role.hmac_key, &role.name)?; + let key = format!("{}{}/", role.secret_id_prefix, role_name_hmac); + let secret_id_hmacs = req.storage_list(&key)?; + + let mut list_items: Vec = Vec::with_capacity(secret_id_hmacs.len()); + for secret_id_hmac in secret_id_hmacs.iter() { + let entry_index = format!("{}{}/{}", role.secret_id_prefix, role_name_hmac, secret_id_hmac); + + // secret_id locks are not indexed by secret_id itself. + // This is because secret_id are not stored in plaintext + // form anywhere in the backend, and hence accessing its + // corresponding lock many times using secret_id is not + // possible. Also, indexing it everywhere using secret_id_hmacs + // makes listing operation easier. + let lock_entry = self.secret_id_locks.get_lock(&secret_id_hmac); + let _locked = lock_entry.lock.read()?; + let storage_entry = req.storage_get(&entry_index)?; + if storage_entry.is_none() { + return Err(RvError::ErrResponse( + "storage entry for SecretID is present but no content found at the index".to_string(), + )); + } + let entry = storage_entry.unwrap(); + let secret_id_entry: SecretIdStorageEntry = serde_json::from_slice(entry.value.as_slice())?; + list_items.push(secret_id_entry.secret_id_accessor); + } + + return Ok(Some(Response::list_response(&list_items))); + } + + return Err(RvError::ErrResponse(format!("role {} does not exist", role_name))); + } + + pub fn update_role_secret_id_common( + &self, + req: &mut Request, + secret_id: &str, + ) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + + if secret_id == "" { + return Err(RvError::ErrResponse("missing secret_id".to_string())); + } + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.read()?; + + let role = self.get_role(req, &role_name)?; + if role.is_none() { + return Err(RvError::ErrResponse(format!("role {} does not exist", role_name))); + } + + let role = role.unwrap(); + + if !role.bind_secret_id { + return Err(RvError::ErrResponse("bind_secret_id is not set on the role".to_string())); + } + + let cidr_list_value = req.get_data_or_default("cidr_list")?; + let cidr_list = cidr_list_value.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + // Validate the list of CIDR blocks + if cidr_list.len() != 0 { + let cidrs: Vec<&str> = cidr_list.iter().map(AsRef::as_ref).collect(); + if !utils::cidr::validate_cidrs(&cidrs)? { + return Err(RvError::ErrResponse("failed to validate CIDR blocks".to_string())); + } + } + + // Ensure that the CIDRs on the secret ID are a subset of that of role's + verify_cidr_role_secret_id_subset(&cidr_list, &role.secret_id_bound_cidrs)?; + + let token_bound_cidrs_value = req.get_data_or_default("token_bound_cidrs")?; + let token_bound_cidrs = + token_bound_cidrs_value.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + // Validate the list of CIDR blocks + if token_bound_cidrs.len() != 0 { + let cidrs: Vec<&str> = token_bound_cidrs.iter().map(AsRef::as_ref).collect(); + if !utils::cidr::validate_cidrs(&cidrs)? { + return Err(RvError::ErrResponse("failed to validate CIDR blocks".to_string())); + } + } + + // Ensure that the token CIDRs on the secret ID are a subset of that of role's + let role_token_bound_cidrs = + role.token_bound_cidrs.iter().map(|s| s.sock_addr.to_string()).collect::>(); + verify_cidr_role_secret_id_subset(&token_bound_cidrs, &role_token_bound_cidrs)?; + + // Check whether or not specified num_uses is defined, otherwise fallback to role's secret_id_num_uses + let num_uses: i64; + if let Ok(num_uses_value) = req.get_data("num_uses") { + num_uses = num_uses_value.as_i64().ok_or(RvError::ErrRequestFieldInvalid)?; + if num_uses < 0 { + return Err(RvError::ErrResponse("num_uses cannot be negative".to_string())); + } + // If the specified num_uses is higher than the role's secret_id_num_uses, throw an error rather than implicitly overriding + if (num_uses == 0 && role.secret_id_num_uses > 0) + || (role.secret_id_num_uses > 0 && num_uses > role.secret_id_num_uses) + { + return Err(RvError::ErrResponse( + "num_uses cannot be higher than the role's secret_id_num_uses".to_string(), + )); + } + } else { + num_uses = role.secret_id_num_uses; + } + + // Check whether or not specified ttl is defined, otherwise fallback to role's secret_id_ttl + let ttl: Duration; + if let Ok(ttl_value) = req.get_data("ttl") { + ttl = ttl_value.as_duration().ok_or(RvError::ErrRequestFieldInvalid)?; + if (ttl.as_secs() == 0 && role.secret_id_ttl.as_secs() > 0) + || (role.secret_id_ttl.as_secs() > 0 && ttl.as_secs() > role.secret_id_ttl.as_secs()) + { + return Err(RvError::ErrResponse("ttl cannot be longer than the role's secret_id_ttl".to_string())); + } + } else { + ttl = role.secret_id_ttl; + } + + let mut secret_id_storage = SecretIdStorageEntry { + secret_id_num_uses: num_uses, + secret_id_ttl: ttl, + cidr_list, + token_cidr_list: token_bound_cidrs, + ..Default::default() + }; + + if let Ok(metadata_value) = req.get_data("metadata") { + secret_id_storage.metadata = metadata_value.as_map().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + let storage = Arc::as_ref(req.storage.as_ref().unwrap()); + self.register_secret_id_entry( + storage, + &role.name, + &secret_id, + &role.hmac_key, + &role.secret_id_prefix, + &mut secret_id_storage, + )?; + + let resp_data = json!({ + "secret_id": secret_id, + "secret_id_accessor": secret_id_storage.secret_id_accessor, + "secret_id_ttl": self.derive_secret_id_ttl(secret_id_storage.secret_id_ttl).as_secs(), + "secret_id_num_uses": secret_id_storage.secret_id_num_uses, + }); + + Ok(Some(Response::data_response(Some(resp_data.as_object().unwrap().clone())))) + } + + pub fn write_role_secret_id(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let secret_id = utils::generate_uuid(); + self.update_role_secret_id_common(req, &secret_id) + } + + pub fn write_role_secret_id_lookup( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + let secret_id = req.get_data_as_str("secret_id")?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.read()?; + + let role = self.get_role(req, &role_name)?; + if role.is_none() { + return Err(RvError::ErrResponse(format!("role {} does not exist", role_name))); + } + + let role = role.unwrap(); + + let role_name_hmac = create_hmac(&role.hmac_key, &role.name)?; + let secret_id_hmac = create_hmac(&role.hmac_key, &secret_id)?; + + let entry_index = format!("{}{}/{}", role.secret_id_prefix, role_name_hmac, secret_id_hmac); + + let lock_entry = self.secret_id_locks.get_lock(&secret_id_hmac); + let _locked = lock_entry.lock.write()?; + + let storage = Arc::as_ref(req.storage.as_ref().unwrap()); + + if let Some(secret_id_entry) = + self.get_secret_id_storage_entry(storage, &role.secret_id_prefix, &role_name_hmac, &secret_id_hmac)? + { + // If a secret ID entry does not have a corresponding accessor + // entry, revoke the secret ID immediately + let accessor_entry = self.get_secret_id_accessor_entry( + storage, + &secret_id_entry.secret_id_accessor, + &role.secret_id_prefix, + )?; + if accessor_entry.is_none() { + req.storage_delete(&entry_index)?; + return Err(RvError::ErrResponse("invalid secret_id".to_string())); + } + + let data = serde_json::to_value(&secret_id_entry)?; + return Ok(Some(Response::data_response(Some(data.as_object().unwrap().clone())))); + } + + Ok(None) + } + + pub fn write_role_secret_id_destory( + &self, + backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.delete_role_secret_id_destory(backend, req) + } + + pub fn delete_role_secret_id_destory( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + let secret_id = req.get_data_as_str("secret_id")?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.read()?; + + let role = self.get_role(req, &role_name)?; + if role.is_none() { + return Err(RvError::ErrResponse(format!("role {} does not exist", role_name))); + } + + let role = role.unwrap(); + + let role_name_hmac = create_hmac(&role.hmac_key, &role.name)?; + let secret_id_hmac = create_hmac(&role.hmac_key, &secret_id)?; + + let entry_index = format!("{}{}/{}", role.secret_id_prefix, role_name_hmac, secret_id_hmac); + + let lock_entry = self.secret_id_locks.get_lock(&secret_id_hmac); + let _locked = lock_entry.lock.write()?; + + let storage = Arc::as_ref(req.storage.as_ref().unwrap()); + + if let Some(secret_id_entry) = + self.get_secret_id_storage_entry(storage, &role.secret_id_prefix, &role_name_hmac, &secret_id_hmac)? + { + // Delete the accessor of the secret_id first + self.delete_secret_id_accessor_entry(storage, &secret_id_entry.secret_id_accessor, &role.secret_id_prefix)?; + + // Delete the storage entry that corresponds to the secret_id + storage.delete(&entry_index)?; + } + + Ok(None) + } + + pub fn write_role_secret_id_accessor_lookup( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + let secret_id_accessor = req.get_data_as_str("secret_id_accessor")?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.read()?; + + let role = self.get_role(req, &role_name)?; + if role.is_none() { + return Err(RvError::ErrResponse(format!("role {} does not exist", role_name))); + } + + let role = role.unwrap(); + + let storage = Arc::as_ref(req.storage.as_ref().unwrap()); + + if let Some(accessor_entry) = + self.get_secret_id_accessor_entry(storage, &secret_id_accessor, &role.secret_id_prefix)? + { + let role_name_hmac = create_hmac(&role.hmac_key, &role.name)?; + + let lock_entry = self.secret_id_locks.get_lock(&accessor_entry.secret_id_hmac); + let _locked = lock_entry.lock.read()?; + + if let Some(secret_id_entry) = self.get_secret_id_storage_entry( + storage, + &role.secret_id_prefix, + &role_name_hmac, + &accessor_entry.secret_id_hmac, + )? { + let data = serde_json::to_value(&secret_id_entry)?; + return Ok(Some(Response::data_response(Some(data.as_object().unwrap().clone())))); + } + } else { + return Err(RvError::ErrResponseStatus( + 404, + format!("failed to find accessor entry for secret_id_accessor: {}", secret_id_accessor), + )); + } + + Ok(None) + } + + pub fn write_role_secret_id_accessor_destory( + &self, + backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.delete_role_secret_id_accessor_destory(backend, req) + } + + pub fn delete_role_secret_id_accessor_destory( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + let role_name = req.get_data_as_str("role_name")?; + let secret_id_accessor = req.get_data_as_str("secret_id_accessor")?; + + let lock_entry = self.role_locks.get_lock(&role_name); + let _locked = lock_entry.lock.write()?; + + // secret_id is indexed based on HMACed role_name and HMACed secret_id. + // Get the role details to fetch the role_id and accessor to get + // the HMACed secret_id. + + let role = self.get_role(req, &role_name)?; + if role.is_none() { + return Err(RvError::ErrResponse(format!("role {} does not exist", role_name))); + } + + let role = role.unwrap(); + + let storage = Arc::as_ref(req.storage.as_ref().unwrap()); + + if let Some(accessor_entry) = + self.get_secret_id_accessor_entry(storage, &secret_id_accessor, &role.secret_id_prefix)? + { + let role_name_hmac = create_hmac(&role.hmac_key, &role.name)?; + + let lock_entry = self.secret_id_locks.get_lock(&accessor_entry.secret_id_hmac); + let _locked = lock_entry.lock.write()?; + + // Verify we have a valid secret_id storage entry + if self + .get_secret_id_storage_entry( + storage, + &role.secret_id_prefix, + &role_name_hmac, + &accessor_entry.secret_id_hmac, + )? + .is_none() + { + return Err(RvError::ErrResponseStatus( + 403, + format!("invalid secret_id_accessor: {}", secret_id_accessor), + )); + } + + let entry_index = format!("{}{}/{}", role.secret_id_prefix, role_name_hmac, &accessor_entry.secret_id_hmac); + + let storage = Arc::as_ref(req.storage.as_ref().unwrap()); + + // Delete the accessor of the secret_id first + self.delete_secret_id_accessor_entry(storage, &secret_id_accessor, &role.secret_id_prefix)?; + + storage.delete(&entry_index)?; + } else { + return Err(RvError::ErrResponseStatus( + 404, + format!("failed to find accessor entry for secret_id_accessor: {}", secret_id_accessor), + )); + } + + Ok(None) + } + + pub fn write_role_custom_secret_id( + &self, + _backend: &dyn Backend, + req: &mut Request, + ) -> Result, RvError> { + self.update_role_secret_id_common(req, req.get_data("secret_id")?.as_str().unwrap_or("")) + } +} + +#[cfg(test)] +mod test { + use std::{ + collections::HashMap, + default::Default, + env, fs, + sync::{Arc, RwLock}, + }; + + use as_any::Downcast; + use go_defer::defer; + use serde_json::{json, Map, Value}; + + use super::{ + super::{AppRoleModule, SECRET_ID_PREFIX}, + *, + }; + use crate::{ + core::{Core, SealConfig}, + logical::{Operation, Request}, + modules::auth::expiration::MAX_LEASE_DURATION_SECS, + storage::{self, Storage}, + }; + + fn test_list_api(core: &Core, token: &str, path: &str, is_ok: bool) -> Result, RvError> { + let mut req = Request::new(path); + req.operation = Operation::List; + req.client_token = token.to_string(); + let resp = core.handle_request(&mut req); + println!("list resp: {:?}", resp); + assert_eq!(resp.is_ok(), is_ok); + resp + } + + fn test_read_api(core: &Core, token: &str, path: &str, is_ok: bool) -> Result, RvError> { + let mut req = Request::new(path); + req.operation = Operation::Read; + req.client_token = token.to_string(); + let resp = core.handle_request(&mut req); + println!("read resp: {:?}", resp); + assert_eq!(resp.is_ok(), is_ok); + resp + } + + fn test_write_api( + core: &Core, + token: &str, + path: &str, + is_ok: bool, + data: Option>, + ) -> Result, RvError> { + let mut req = Request::new(path); + req.operation = Operation::Write; + req.client_token = token.to_string(); + req.body = data; + + let resp = core.handle_request(&mut req); + println!("write resp: {:?}", resp); + assert_eq!(resp.is_ok(), is_ok); + resp + } + + fn test_delete_api( + core: &Core, + token: &str, + path: &str, + is_ok: bool, + data: Option>, + ) -> Result, RvError> { + let mut req = Request::new(path); + req.operation = Operation::Delete; + req.client_token = token.to_string(); + req.body = data; + let resp = core.handle_request(&mut req); + println!("delete resp: {:?}", resp); + assert_eq!(resp.is_ok(), is_ok); + resp + } + + fn mount_approle_auth(core: Arc>, token: &str, path: &str) { + let core = core.read().unwrap(); + + let auth_data = json!({ + "type": "approle", + }) + .as_object() + .unwrap() + .clone(); + + let resp = test_write_api(&core, token, format!("sys/auth/{}", path).as_str(), true, Some(auth_data)); + assert!(resp.is_ok()); + } + + fn test_write_role( + core: Arc>, + token: &str, + path: &str, + role_name: &str, + role_id: &str, + policies: &str, + expect: bool, + ) { + let core = core.read().unwrap(); + + let mut role_data = json!({ + "role_id": role_id, + "policies": policies, + "secret_id_num_uses": 10, + "secret_id_ttl": 300, + "token_ttl": 400, + "token_max_ttl": 500, + }) + .as_object() + .unwrap() + .clone(); + + if role_id == "" { + role_data.remove("role_id"); + } + + let _ = + test_write_api(&core, token, format!("auth/{}/role/{}", path, role_name).as_str(), expect, Some(role_data)); + } + + fn test_delete_role(core: Arc>, token: &str, path: &str, role_name: &str) { + let core = core.read().unwrap(); + + let resp = test_delete_api(&core, token, format!("auth/{}/role/{}", path, role_name).as_str(), true, None); + assert!(resp.is_ok()); + } + + fn generate_secret_id(core: Arc>, token: &str, path: &str, role_name: &str) -> (String, String) { + let core = core.read().unwrap(); + let resp = + test_write_api(&core, token, format!("auth/{}/role/{}/secret-id", path, role_name).as_str(), true, None); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id = resp_data["secret_id"].as_str().unwrap(); + let secret_id_accessor = resp_data["secret_id_accessor"].as_str().unwrap(); + + (secret_id.to_string(), secret_id_accessor.to_string()) + } + + fn rusty_vault_init(dir: &str) -> (String, Arc>) { + let root_token; + + println!("rusty_vault_init, dir: {}", dir); + + let mut conf: HashMap = HashMap::new(); + conf.insert("path".to_string(), Value::String(dir.to_string())); + + let backend = storage::new_backend("file", &conf).unwrap(); + + let barrier = storage::barrier_aes_gcm::AESGCMBarrier::new(Arc::clone(&backend)); + + let c = Arc::new(RwLock::new(Core { physical: backend, barrier: Arc::new(barrier), ..Default::default() })); + + { + let mut core = c.write().unwrap(); + assert!(core.config(Arc::clone(&c), None).is_ok()); + + let seal_config = SealConfig { secret_shares: 10, secret_threshold: 5 }; + + let result = core.init(&seal_config); + assert!(result.is_ok()); + let init_result = result.unwrap(); + println!("init_result: {:?}", init_result); + + let mut unsealed = false; + for i in 0..seal_config.secret_threshold { + let key = &init_result.secret_shares[i as usize]; + let unseal = core.unseal(key); + assert!(unseal.is_ok()); + unsealed = unseal.unwrap(); + } + + root_token = init_result.root_token; + println!("root_token: {:?}", root_token); + + assert!(unsealed); + } + + (root_token, c) + } + + fn test_login( + core: Arc>, + path: &str, + role_id: &str, + secret_id: &str, + is_ok: bool, + ) -> Result, RvError> { + let core = core.read().unwrap(); + + let data = json!({ + "role_id": role_id, + "secret_id": secret_id, + }) + .as_object() + .unwrap() + .clone(); + + let mut req = Request::new(format!("auth/{}/login", path).as_str()); + req.operation = Operation::Write; + req.body = Some(data); + + let resp = core.handle_request(&mut req); + if is_ok { + assert!(resp.is_ok()); + let resp = resp.as_ref().unwrap(); + assert!(resp.is_some()); + let resp = resp.as_ref().unwrap(); + assert!(resp.auth.is_some()); + } else { + assert!(resp.is_err()); + } + + resp + } + + #[test] + fn test_approle_read_local_secret_ids() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_read_local_secret_ids"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + // Create a role + let data = json!({ + "local_secret_ids": true, + "bind_secret_id": true, + }) + .as_object() + .unwrap() + .clone(); + + let c = core.read().unwrap(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole", true, Some(data.clone())); + assert!(resp.is_ok()); + + // Get the role field + let resp = test_read_api(&c, &root_token, "auth/approle/role/testrole/local-secret-ids", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["local_secret_ids"].as_bool().unwrap(), data["local_secret_ids"].as_bool().unwrap()); + } + + #[test] + fn test_approle_local_non_secret_ids() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_local_non_secret_ids"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + // Create a role with local_secret_ids set + let data = json!({ + "policies": ["default", "role1policy"], + "local_secret_ids": true, + "bind_secret_id": true, + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole1", true, Some(data.clone())); + assert!(resp.is_ok()); + + // Create another role without setting local_secret_ids + let data = json!({ + "policies": ["default", "role1policy"], + "bind_secret_id": true, + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole2", true, Some(data.clone())); + assert!(resp.is_ok()); + + // Create secret IDs on testrole1 + let len = 10; + for _i in 0..len { + assert!(test_write_api(&c, &root_token, "auth/approle/role/testrole1/secret-id", true, None).is_ok()); + } + + // Check the number of secret IDs generated + let resp = test_list_api(&c, &root_token, "auth/approle/role/testrole1/secret-id", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert!(resp_data["keys"].is_array()); + assert_eq!(resp_data["keys"].as_array().unwrap().len(), len); + + // Create secret IDs on testrole2 + for _i in 0..len { + assert!(test_write_api(&c, &root_token, "auth/approle/role/testrole2/secret-id", true, None).is_ok()); + } + + // Check the number of secret IDs generated + let resp = test_list_api(&c, &root_token, "auth/approle/role/testrole2/secret-id", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert!(resp_data["keys"].is_array()); + assert_eq!(resp_data["keys"].as_array().unwrap().len(), len); + } + + #[test] + fn test_approle_upgrade_secret_id_prefix() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_upgrade_secret_id_prefix"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let module = c.module_manager.get_module("approle").unwrap(); + let approle_mod = module.read().unwrap(); + let approle_module = approle_mod.as_ref().downcast_ref::().unwrap(); + + let mut req = Request::new("/auth/approle/testrole"); + req.operation = Operation::Write; + req.storage = c.get_system_view().map(|arc| arc as Arc); + + let role_entry = RoleEntry { + role_id: "testroleid".to_string(), + hmac_key: "testhmackey".to_string(), + bind_secret_id: true, + bound_cidr_list_old: "127.0.0.1/18,192.178.1.2/24".to_string(), + ..Default::default() + }; + let resp = approle_module.set_role(&mut req, "testrole", &role_entry, ""); + assert!(resp.is_ok()); + + // Reading the role entry should upgrade it to contain secret_id_prefix + let resp = approle_module.get_role(&mut req, "testrole"); + assert!(resp.is_ok()); + let role_entry = resp.unwrap().unwrap(); + assert_ne!(role_entry.secret_id_prefix, ""); + + // Ensure that the API response contains local_secret_ids + req.operation = Operation::Read; + req.path = "auth/approle/role/testrole".to_string(); + req.client_token = root_token.to_string(); + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + + let mock_backend = approle_module.new_backend(); + let resp = approle_module.read_role(&mock_backend, &mut req); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert!(!resp_data["local_secret_ids"].as_bool().unwrap()); + } + + #[test] + fn test_approle_local_secret_id_immutablility() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_local_secret_id_immutablility"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + // Create a role with local_secret_ids set + let data = json!({ + "policies": ["default"], + "bind_secret_id": true, + "local_secret_ids": true, + "bound_cidr_list": ["127.0.0.1/18", "192.178.1.2/24"], + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole", true, Some(data.clone())); + assert!(resp.is_ok()); + + // Attempt to modify local_secret_ids should fail + let _ = test_write_api(&c, &root_token, "auth/approle/role/testrole", false, Some(data.clone())); + } + + #[test] + fn test_approle_upgrade_bound_cidr_list() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_upgrade_bound_cidr_list"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + // Create a role with bound_cidr_list set + let data = json!({ + "policies": ["default"], + "bind_secret_id": true, + "bound_cidr_list": ["127.0.0.1/18", "192.178.1.2/24"], + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole", true, Some(data.clone())); + assert!(resp.is_ok()); + + // Read the role and check that the bound_cidr_list is set properly + let resp = test_read_api(&c, &root_token, "auth/approle/role/testrole", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let expected: Vec = + data["bound_cidr_list"].as_comma_string_slice().unwrap().iter().map(|s| Value::String(s.clone())).collect(); + assert_eq!(resp_data["secret_id_bound_cidrs"].as_array().unwrap().clone(), expected); + + let module = c.module_manager.get_module("approle").unwrap(); + let approle_mod = module.read().unwrap(); + let approle_module = approle_mod.as_ref().downcast_ref::().unwrap(); + + let mut req = Request::new("/auth/approle/testrole"); + req.operation = Operation::Write; + req.storage = c.get_system_view().map(|arc| arc as Arc); + + // Modify the storage entry of the role to hold the old style string typed bound_cidr_list + let role_entry = RoleEntry { + role_id: "testroleid".to_string(), + hmac_key: "testhmackey".to_string(), + bind_secret_id: true, + bound_cidr_list_old: "127.0.0.1/18,192.178.1.2/24".to_string(), + secret_id_prefix: SECRET_ID_PREFIX.to_string(), + ..Default::default() + }; + let resp = approle_module.set_role(&mut req, "testrole", &role_entry, ""); + assert!(resp.is_ok()); + let expected: Vec = role_entry.bound_cidr_list_old.split(',').map(|s| s.to_string()).collect(); + + // Read the role. The upgrade code should have migrated the old type to the new type + let resp = approle_module.get_role(&mut req, "testrole"); + assert!(resp.is_ok()); + let role_entry = resp.unwrap().unwrap(); + assert_eq!(role_entry.secret_id_bound_cidrs, expected); + assert_eq!(role_entry.bound_cidr_list_old.len(), 0); + assert_eq!(role_entry.bound_cidr_list.len(), 0); + + // Create a secret-id by supplying a subset of the role's CIDR blocks with the new type + let data = json!({ + "cidr_list": ["127.0.0.1/24"], + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole/secret-id", true, Some(data)); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id = resp_data["secret_id"].as_str().unwrap(); + assert_ne!(secret_id, ""); + } + + #[test] + fn test_approle_role_name_lower_casing() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_name_lower_casing"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let module = c.module_manager.get_module("approle").unwrap(); + let approle_mod = module.read().unwrap(); + let approle_module = approle_mod.as_ref().downcast_ref::().unwrap(); + + let mut req = Request::new("/auth/approle/testrole"); + req.operation = Operation::Write; + req.storage = c.get_system_view().map(|arc| arc as Arc); + + // Create a role with lower_case_role_name is false + let role_entry = RoleEntry { + role_id: "testroleid".to_string(), + hmac_key: "testhmackey".to_string(), + bind_secret_id: true, + lower_case_role_name: false, + secret_id_prefix: SECRET_ID_PREFIX.to_string(), + ..Default::default() + }; + let resp = approle_module.set_role(&mut req, "testRoleName", &role_entry, ""); + assert!(resp.is_ok()); + + req.operation = Operation::Write; + req.path = "auth/approle/role/testRoleName/secret-id".to_string(); + req.client_token = root_token.to_string(); + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + + let mock_backend = approle_module.new_backend(); + let resp = approle_module.write_role_secret_id(&mock_backend, &mut req); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id = resp_data["secret_id"].as_str().unwrap(); + let role_id = "testroleid"; + + // Regular login flow. This should succeed + let data = json!({ + "role_id": role_id, + "secret_id": secret_id, + }) + .as_object() + .unwrap() + .clone(); + req.path = "auth/approle/login".to_string(); + req.operation = Operation::Write; + req.body = Some(data); + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + let resp = approle_module.login(&mock_backend, &mut req); + assert!(resp.is_ok()); + + // Lower case the role name when generating the secret id + req.path = "auth/approle/role/testrolename/secret-id".to_string(); + req.operation = Operation::Write; + req.body = None; + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + let resp = approle_module.write_role_secret_id(&mock_backend, &mut req); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id = resp_data["secret_id"].as_str().unwrap(); + + // Login should fail + let data = json!({ + "role_id": role_id, + "secret_id": secret_id, + }) + .as_object() + .unwrap() + .clone(); + req.path = "auth/approle/login".to_string(); + req.operation = Operation::Write; + req.body = Some(data); + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + let resp = approle_module.login(&mock_backend, &mut req); + assert!(resp.is_err()); + + // Delete the role and create it again. This time don't directly persist + // it, but route the request to the creation handler so that it sets the + // lower_case_role_name to true. + req.path = "auth/approle/role/testRoleName".to_string(); + req.operation = Operation::Delete; + req.body = None; + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + let resp = approle_module.delete_role(&mock_backend, &mut req); + assert!(resp.is_ok()); + + let data = json!({ + "policies": ["default"], + "bind_secret_id": true, + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testRoleName", true, Some(data)); + assert!(resp.is_ok()); + + // Create secret id with lower cased role name + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrolename/secret-id", true, None); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id = resp_data["secret_id"].as_str().unwrap(); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/testrolename/role-id", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let role_id = resp_data["role_id"].as_str().unwrap(); + + // Login should pass + let _ = test_login(Arc::clone(&core), "approle", &role_id, &secret_id, true); + + // Lookup of secret ID should work in case-insensitive manner + let data = json!({ + "secret_id": secret_id, + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrolename/secret-id/lookup", true, Some(data)); + assert!(resp.is_ok()); + + // Listing of secret IDs should work in case-insensitive manner + let resp = test_list_api(&c, &root_token, "auth/approle/role/testrolename/secret-id", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let keys = resp_data["keys"].as_array().unwrap(); + assert_eq!(keys.len(), 1); + } + + #[test] + fn test_approle_role_read_set_index() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_read_set_index"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let module = c.module_manager.get_module("approle").unwrap(); + let approle_mod = module.read().unwrap(); + let approle_module = approle_mod.as_ref().downcast_ref::().unwrap(); + let mock_backend = approle_module.new_backend(); + + // Create a role + let mut req = Request::new("/auth/approle/testrole"); + req.operation = Operation::Write; + req.storage = c.get_system_view().map(|arc| arc as Arc); + let role_entry = RoleEntry { + role_id: "testroleid".to_string(), + hmac_key: "testhmackey".to_string(), + bind_secret_id: true, + secret_id_prefix: SECRET_ID_PREFIX.to_string(), + ..Default::default() + }; + let resp = approle_module.set_role(&mut req, "testrole", &role_entry, ""); + assert!(resp.is_ok()); + + // Get the role ID + req.operation = Operation::Read; + req.path = "auth/approle/role/testrole/role-id".to_string(); + req.client_token = root_token.to_string(); + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + let resp = approle_module.read_role_role_id(&mock_backend, &mut req); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let role_id = resp_data["role_id"].as_str().unwrap(); + + // Delete the role ID index + req.operation = Operation::Write; + req.path = "auth/approle/role/testrole/role-id".to_string(); + req.client_token = root_token.to_string(); + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + let resp = approle_module.delete_role_id(&mut req, &role_id); + assert!(resp.is_ok()); + + // Read the role again. This should add the index and return a warning + req.operation = Operation::Read; + req.path = "auth/approle/role/testrole".to_string(); + req.client_token = root_token.to_string(); + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + let resp = approle_module.read_role(&mock_backend, &mut req); + assert!(resp.is_ok()); + let resp = resp.unwrap().unwrap(); + assert!(resp.warnings.contains(&"Role identifier was missing an index back to role name".to_string())); + + // Check if the index has been successfully created + req.storage = c.get_system_view().map(|arc| arc as Arc); + let role_id_entry = approle_module.get_role_id(&mut req, &role_id); + assert!(role_id_entry.is_ok()); + let role_id_entry = role_id_entry.unwrap().unwrap(); + assert_eq!(role_id_entry.name, "testrole"); + + // Check if updating and reading of roles work and that there are no lock + // contentions dangling due to previous operation + let data = json!({ + "policies": ["default"], + "bind_secret_id": true, + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole", true, Some(data)); + assert!(resp.is_ok()); + let resp = test_read_api(&c, &root_token, "auth/approle/role/testrole", true); + assert!(resp.is_ok()); + } + + #[test] + fn test_approle_cidr_subset() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_cidr_subset"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let mut role_data = json!({ + "role_id": "role-id-123", + "policies": "a,b", + "bound_cidr_list": "127.0.0.1/24", + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole1", true, Some(role_data.clone())); + assert!(resp.is_ok()); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/testrole", true); + assert!(resp.is_ok()); + + let mut secret_data = json!({ + "cidr_list": ["127.0.0.1/16"], + }) + .as_object() + .unwrap() + .clone(); + let resp = + test_write_api(&c, &root_token, "auth/approle/role/testrole1/secret-id", false, Some(secret_data.clone())); + assert!(resp.is_err()); + + role_data["bound_cidr_list"] = Value::from("192.168.27.29/16,172.245.30.40/24,10.20.30.40/30"); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole1", true, Some(role_data)); + assert!(resp.is_ok()); + + secret_data["cidr_list"] = Value::from("192.168.27.29/20,172.245.30.40/25,10.20.30.40/32"); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole1/secret-id", true, Some(secret_data)); + assert!(resp.is_ok()); + } + + #[test] + fn test_approle_token_bound_cidr_subset_32_mask() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_token_bound_cidr_subset_32_mask"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let role_data = json!({ + "role_id": "role-id-123", + "policies": "a,b", + "token_bound_cidrs": "127.0.0.1/32", + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole1", true, Some(role_data.clone())); + assert!(resp.is_ok()); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/testrole", true); + assert!(resp.is_ok()); + + let mut secret_data = json!({ + "token_bound_cidrs": ["127.0.0.1/32"], + }) + .as_object() + .unwrap() + .clone(); + let resp = + test_write_api(&c, &root_token, "auth/approle/role/testrole1/secret-id", true, Some(secret_data.clone())); + assert!(resp.is_ok()); + + secret_data["token_bound_cidrs"] = Value::from("127.0.0.1/24"); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole1/secret-id", false, Some(secret_data)); + assert!(resp.is_err()); + } + + #[test] + fn test_approle_role_constraints() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_constraints"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + // Set bind_secret_id, which is enabled by default + let mut role_data = json!({ + "role_id": "role-id-123", + "policies": "a,b", + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole1", true, Some(role_data.clone())); + assert!(resp.is_ok()); + + // Set bound_cidr_list alone by explicitly disabling bind_secret_id + role_data.insert("bind_secret_id".to_string(), Value::from(false)); + role_data.insert("token_bound_cidrs".to_string(), Value::from("0.0.0.0/0")); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole1", true, Some(role_data.clone())); + assert!(resp.is_ok()); + + // Remove both constraints + role_data["bind_secret_id"] = Value::from(false); + role_data["token_bound_cidrs"] = Value::from(""); + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole1", false, Some(role_data.clone())); + assert!(resp.is_err()); + } + + #[test] + fn test_approle_update_role_id() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_update_role_id"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + test_write_role(core.clone(), &root_token, "approle", "testrole1", "role-id-123", "a,b", true); + + let role_id_data = json!({ + "role_id": "customroleid", + }) + .as_object() + .unwrap() + .clone(); + let resp = + test_write_api(&c, &root_token, "auth/approle/role/testrole1/role-id", true, Some(role_id_data.clone())); + assert!(resp.is_ok()); + + let resp = test_write_api(&c, &root_token, "auth/approle/role/testrole1/secret-id", true, None); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id = resp_data["secret_id"].as_str().unwrap(); + + // Login should fail + let _ = test_login(Arc::clone(&core), "approle", "role-id-123", &secret_id, false); + + // Login should pass + let _ = test_login(Arc::clone(&core), "approle", "customroleid", &secret_id, true); + } + + #[test] + fn test_approle_role_id_uniqueness() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_id_uniqueness"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + test_write_role(core.clone(), &root_token, "approle", "testrole1", "role-id-123", "a,b", true); + + test_write_role(core.clone(), &root_token, "approle", "testrole2", "role-id-123", "a,b", false); + + test_write_role(core.clone(), &root_token, "approle", "testrole2", "role-id-456", "a,b", true); + + test_write_role(core.clone(), &root_token, "approle", "testrole2", "role-id-123", "a,b", false); + + test_write_role(core.clone(), &root_token, "approle", "testrole1", "role-id-456", "a,b", false); + + let mut role_id_data = json!({ + "role_id": "role-id-456", + }) + .as_object() + .unwrap() + .clone(); + let resp = + test_write_api(&c, &root_token, "auth/approle/role/testrole1/role-id", false, Some(role_id_data.clone())); + assert!(resp.is_err()); + + role_id_data["role_id"] = Value::from("role-id-123"); + let resp = + test_write_api(&c, &root_token, "auth/approle/role/testrole2/role-id", false, Some(role_id_data.clone())); + assert!(resp.is_err()); + + role_id_data["role_id"] = Value::from("role-id-2000"); + let resp = + test_write_api(&c, &root_token, "auth/approle/role/testrole2/role-id", true, Some(role_id_data.clone())); + assert!(resp.is_ok()); + + role_id_data["role_id"] = Value::from("role-id-1000"); + let resp = + test_write_api(&c, &root_token, "auth/approle/role/testrole1/role-id", true, Some(role_id_data.clone())); + assert!(resp.is_ok()); + } + + #[test] + fn test_approle_role_delete_secret_id() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_delete_secret_id"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + test_write_role(core.clone(), &root_token, "approle", "role1", "", "a,b", true); + + let _ = generate_secret_id(core.clone(), &root_token, "approle", "role1"); + let _ = generate_secret_id(core.clone(), &root_token, "approle", "role1"); + let _ = generate_secret_id(core.clone(), &root_token, "approle", "role1"); + + let resp = test_list_api(&c, &root_token, "auth/approle/role/role1/secret-id", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let keys = resp_data["keys"].as_array().unwrap(); + assert_eq!(keys.len(), 3); + + test_delete_role(core.clone(), &root_token, "approle", "role1"); + let _ = test_list_api(&c, &root_token, "auth/approle/role/role1/secret-id", false); + } + + #[test] + fn test_approle_lookup_and_destroy_role_secret_id() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_lookup_and_destroy_role_secret_id"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + test_write_role(core.clone(), &root_token, "approle", "role1", "", "a,b", true); + + let (secret_id, _) = generate_secret_id(core.clone(), &root_token, "approle", "role1"); + + let secret_id_data = json!({ + "secret_id": secret_id, + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api( + &c, + &root_token, + "auth/approle/role/role1/secret-id/lookup", + true, + Some(secret_id_data.clone()), + ); + assert!(resp.unwrap().unwrap().data.is_some()); + + let _ = test_delete_api( + &c, + &root_token, + "auth/approle/role/role1/secret-id/destroy", + true, + Some(secret_id_data.clone()), + ); + let resp = test_write_api( + &c, + &root_token, + "auth/approle/role/role1/secret-id/lookup", + true, + Some(secret_id_data.clone()), + ); + assert!(resp.unwrap().is_none()); + } + + #[test] + fn test_approle_lookup_and_destroy_role_secret_id_accessor() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_lookup_and_destroy_role_secret_id_accessor"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + test_write_role(core.clone(), &root_token, "approle", "role1", "", "a,b", true); + + let _ = generate_secret_id(core.clone(), &root_token, "approle", "role1"); + + let resp = test_list_api(&c, &root_token, "auth/approle/role/role1/secret-id", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let keys = resp_data["keys"].as_array().unwrap(); + assert_eq!(keys.len(), 1); + + let hmac_secret_id = keys[0].as_str().unwrap(); + let hmac_data = json!({ + "secret_id_accessor": hmac_secret_id, + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api( + &c, + &root_token, + "auth/approle/role/role1/secret-id-accessor/lookup", + true, + Some(hmac_data.clone()), + ); + assert!(resp.unwrap().unwrap().data.is_some()); + + let _ = test_delete_api( + &c, + &root_token, + "auth/approle/role/role1/secret-id-accessor/destroy", + true, + Some(hmac_data.clone()), + ); + let _ = test_write_api( + &c, + &root_token, + "auth/approle/role/role1/secret-id-accessor/lookup", + false, + Some(hmac_data.clone()), + ); + } + + #[test] + fn test_approle_lookup_role_secret_id_accessor() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_lookup_role_secret_id_accessor"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + test_write_role(core.clone(), &root_token, "approle", "role1", "", "a,b", true); + + let hmac_data = json!({ + "secret_id_accessor": "invalid", + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api( + &c, + &root_token, + "auth/approle/role/role1/secret-id-accessor/lookup", + false, + Some(hmac_data.clone()), + ); + // TODO: resp should ok + } + + #[test] + fn test_approle_list_role_secret_id() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_list_role_secret_id"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + test_write_role(core.clone(), &root_token, "approle", "role1", "", "a,b", true); + + // Create 5 'secret_id's + let _ = generate_secret_id(core.clone(), &root_token, "approle", "role1"); + let _ = generate_secret_id(core.clone(), &root_token, "approle", "role1"); + let _ = generate_secret_id(core.clone(), &root_token, "approle", "role1"); + let _ = generate_secret_id(core.clone(), &root_token, "approle", "role1"); + let _ = generate_secret_id(core.clone(), &root_token, "approle", "role1"); + + let resp = test_list_api(&c, &root_token, "auth/approle/role/role1/secret-id/", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let keys = resp_data["keys"].as_array().unwrap(); + assert_eq!(keys.len(), 5); + } + + #[test] + fn test_approle_list_role() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_list_role"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + test_write_role(core.clone(), &root_token, "approle", "role1", "", "a,b", true); + test_write_role(core.clone(), &root_token, "approle", "role2", "", "c,d", true); + test_write_role(core.clone(), &root_token, "approle", "role3", "", "e,f", true); + test_write_role(core.clone(), &root_token, "approle", "role4", "", "g,h", true); + test_write_role(core.clone(), &root_token, "approle", "role5", "", "i,j", true); + + let resp = test_list_api(&c, &root_token, "auth/approle/role", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let mut keys = resp_data["keys"].as_array().unwrap().clone(); + keys.sort_by(|a, b| a.as_str().unwrap_or("").cmp(b.as_str().unwrap_or(""))); + assert_eq!(keys.len(), 5); + let expect = json!(["role1", "role2", "role3", "role4", "role5"]); + assert_eq!(expect.as_array().unwrap().clone(), keys); + } + + #[test] + fn test_approle_role_secret_id_without_fields() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_secret_id_without_fields"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let role_data = json!({ + "policies": "p,q,r,s", + "secret_id_num_uses": 10, + "secret_id_ttl": 300, + "token_ttl": 400, + "token_max_ttl": 500, + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1", true, Some(role_data.clone())); + + let resp = test_write_api(&c, &root_token, "auth/approle/role/role1/secret-id", true, None); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id = resp_data["secret_id"].as_str().unwrap(); + let secret_id_ttl = resp_data["secret_id_ttl"].as_int().unwrap(); + let secret_id_num_uses = resp_data["secret_id_num_uses"].as_int().unwrap(); + assert_ne!(secret_id, ""); + assert_eq!(secret_id_ttl, role_data["secret_id_ttl"].as_int().unwrap()); + assert_eq!(secret_id_num_uses, role_data["secret_id_num_uses"].as_int().unwrap()); + + let secret_id_data = json!({ + "secret_id": "abcd123", + }) + .as_object() + .unwrap() + .clone(); + let resp = test_write_api( + &c, + &root_token, + "auth/approle/role/role1/custom-secret-id", + true, + Some(secret_id_data.clone()), + ); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id = resp_data["secret_id"].as_str().unwrap(); + let secret_id_ttl = resp_data["secret_id_ttl"].as_int().unwrap(); + let secret_id_num_uses = resp_data["secret_id_num_uses"].as_int().unwrap(); + assert_eq!(secret_id, secret_id_data["secret_id"].as_str().unwrap()); + assert_eq!(secret_id_ttl, role_data["secret_id_ttl"].as_int().unwrap()); + assert_eq!(secret_id_num_uses, role_data["secret_id_num_uses"].as_int().unwrap()); + } + + #[test] + fn test_approle_role_secret_id_with_valid_fields() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_secret_id_with_valid_fields"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let role_data = json!({ + "policies": "p,q,r,s", + "secret_id_num_uses": 0, + "secret_id_ttl": 0, + "token_ttl": 400, + "token_max_ttl": 500, + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1", true, Some(role_data.clone())); + + let cases = vec![ + json!({"name": "finite num_uses and ttl", "payload": {"secret_id": "finite", "ttl": 5, "num_uses": 5}}), + json!({"name": "infinite num_uses and ttl", "payload": {"secret_id": "infinite", "ttl": 0, "num_uses": 0}}), + json!({"name": "finite num_uses and infinite ttl", "payload": {"secret_id": "maxed1", "ttl": 0, "num_uses": 5}}), + json!({"name": "infinite num_uses and finite ttl", "payload": {"secret_id": "maxed2", "ttl": 5, "num_uses": 0}}), + ]; + + for case in cases.iter() { + let secret_id_data = case["payload"].as_object().unwrap().clone(); + let resp = test_write_api( + &c, + &root_token, + "auth/approle/role/role1/secret-id", + true, + Some(secret_id_data.clone()), + ); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id = resp_data["secret_id"].as_str().unwrap(); + let secret_id_ttl = resp_data["secret_id_ttl"].as_int().unwrap(); + let secret_id_num_uses = resp_data["secret_id_num_uses"].as_int().unwrap(); + assert_ne!(secret_id, ""); + assert_eq!(secret_id_ttl, secret_id_data["ttl"].as_int().unwrap()); + assert_eq!(secret_id_num_uses, secret_id_data["num_uses"].as_int().unwrap()); + + let resp = test_write_api( + &c, + &root_token, + "auth/approle/role/role1/custom-secret-id", + true, + Some(secret_id_data.clone()), + ); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id = resp_data["secret_id"].as_str().unwrap(); + let secret_id_ttl = resp_data["secret_id_ttl"].as_int().unwrap(); + let secret_id_num_uses = resp_data["secret_id_num_uses"].as_int().unwrap(); + assert_eq!(secret_id, secret_id_data["secret_id"].as_str().unwrap()); + assert_eq!(secret_id_ttl, secret_id_data["ttl"].as_int().unwrap()); + assert_eq!(secret_id_num_uses, secret_id_data["num_uses"].as_int().unwrap()); + } + } + + #[test] + fn test_approle_role_secret_id_with_invalid_fields() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_secret_id_with_invalid_fields"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let cases = vec![ + json!({ + "name": "infinite role secret id ttl", + "options": { + "secret_id_num_uses": 1, + "secret_id_ttl": 0, + }, + "cases": [{ + "name": "higher num_uses", + "payload": {"secret_id": "abcd123", "ttl": 0, "num_uses": 2}, + "expected": "num_uses cannot be higher than the role's secret_id_num_uses", + }], + }), + json!({ + "name": "infinite role num_uses", + "options": { + "secret_id_num_uses": 0, + "secret_id_ttl": 1, + }, + "cases": [{ + "name": "longer ttl", + "payload": {"secret_id": "abcd123", "ttl": 2, "num_uses": 0}, + "expected": "ttl cannot be longer than the role's secret_id_ttl", + }], + }), + json!({ + "name": "finite role ttl and num_uses", + "options": { + "secret_id_num_uses": 2, + "secret_id_ttl": 2, + }, + "cases": [{ + "name": "infinite ttl", + "payload": {"secret_id": "abcd123", "ttl": 0, "num_uses": 1}, + "expected": "ttl cannot be longer than the role's secret_id_ttl", + }, + { + "name": "infinite num_uses", + "payload": {"secret_id": "abcd123", "ttl": 1, "num_uses": 0}, + "expected": "num_uses cannot be higher than the role's secret_id_num_uses", + }], + }), + json!({ + "name": "mixed role ttl and num_uses", + "options": { + "secret_id_num_uses": 400, + "secret_id_ttl": 500, + }, + "cases": [{ + "name": "negative num_uses", + "payload": {"secret_id": "abcd123", "ttl": 0, "num_uses": -1}, + "expected": "num_uses cannot be negative", + }], + }), + ]; + + for (i, case) in cases.iter().enumerate() { + let mut role_data = json!({ + "policies": "p,q,r,s", + "secret_id_num_uses": 0, + "secret_id_ttl": 0, + "token_ttl": 400, + "token_max_ttl": 500, + }) + .as_object() + .unwrap() + .clone(); + role_data["secret_id_num_uses"] = case["options"]["secret_id_num_uses"].clone(); + role_data["secret_id_ttl"] = case["options"]["secret_id_ttl"].clone(); + let _ = test_write_api( + &c, + &root_token, + format!("auth/approle/role/role{}", i).as_str(), + true, + Some(role_data.clone()), + ); + + for tc in case["cases"].as_array().unwrap().iter() { + let secret_id_data = tc["payload"].as_object().unwrap().clone(); + let resp = test_write_api( + &c, + &root_token, + format!("auth/approle/role/role{}/secret-id", i).as_str(), + false, + Some(secret_id_data.clone()), + ); + if let Err(RvError::ErrResponse(err_text)) = resp { + assert_eq!(err_text, tc["expected"].as_str().unwrap()); + } + let resp = test_write_api( + &c, + &root_token, + format!("auth/approle/role/role{}/custom-secret-id", i).as_str(), + false, + Some(secret_id_data.clone()), + ); + if let Err(RvError::ErrResponse(err_text)) = resp { + assert_eq!(err_text, tc["expected"].as_str().unwrap()); + } + } + } + } + + #[test] + fn test_approle_role_crud() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_crud"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let req_data = json!({ + "policies": "p,q,r,s", + "secret_id_num_uses": 10, + "secret_id_ttl": 300, + "token_ttl": 400, + "token_max_ttl": 500, + "token_num_uses": 600, + "secret_id_bound_cidrs": "127.0.0.1/32,127.0.0.1/16", + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + + let expected = json!({ + "bind_secret_id": true, + "local_secret_ids": false, + "policies": ["p", "q", "r", "s"], + "secret_id_num_uses": 10, + "secret_id_ttl": 300, + "token_ttl": 400, + "token_max_ttl": 500, + "token_num_uses": 600, + "token_no_default_policy": false, + "secret_id_bound_cidrs": ["127.0.0.1/32", "127.0.0.1/16"], + "token_period": 0, + "token_explicit_max_ttl":0, + "token_bound_cidrs": [], + "token_policies": ["p", "q", "r", "s"], + "token_type": "default", + }); + assert_eq!(expected.as_object().unwrap().clone(), resp_data); + + let req_data = json!({ + "role_id": "test_role_id", + "policies": "a,b,c,d", + "secret_id_num_uses": 100, + "secret_id_ttl": 3000, + "token_ttl": 4000, + "token_max_ttl": 5000, + "period": "5m", + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + + let expected = json!({ + "bind_secret_id": true, + "local_secret_ids": false, + "policies": ["a", "b", "c", "d"], + "secret_id_num_uses": 100, + "secret_id_ttl": 3000, + "token_ttl": 4000, + "token_max_ttl": 5000, + "token_num_uses": 600, + "token_no_default_policy": false, + "secret_id_bound_cidrs": ["127.0.0.1/32", "127.0.0.1/16"], + "period": 300, + "token_period": 300, + "token_explicit_max_ttl":0, + "token_bound_cidrs": [], + "token_policies": ["a", "b", "c", "d"], + "token_type": "default", + }); + assert_eq!(expected.as_object().unwrap().clone(), resp_data); + + // RU for role_id field + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/role-id", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let role_id = resp_data["role_id"].as_str().unwrap(); + assert_eq!(role_id, req_data["role_id"].as_str().unwrap()); + + let req_data = json!({ + "role_id": "custom_role_id", + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1/role-id", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/role-id", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["role_id"].as_str().unwrap(), req_data["role_id"].as_str().unwrap()); + + // RUD for bind_secret_id field + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/bind-secret-id", true); + assert!(resp.is_ok()); + + let req_data = json!({ + "bind_secret_id": false, + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1/bind-secret-id", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/bind-secret-id", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["bind_secret_id"].as_bool().unwrap(), req_data["bind_secret_id"].as_bool().unwrap()); + + let _ = test_delete_api(&c, &root_token, "auth/approle/role/role1/bind-secret-id", true, None); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/bind-secret-id", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["bind_secret_id"].as_bool().unwrap(), true); + + // RUD for policies field + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/policies", true); + assert!(resp.is_ok()); + + let req_data = json!({ + "policies": "a1,b1,c1,d1", + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1/policies", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/policies", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!( + resp_data["policies"].as_comma_string_slice().unwrap(), + req_data["policies"].as_comma_string_slice().unwrap() + ); + assert_eq!( + resp_data["token_policies"].as_comma_string_slice().unwrap(), + req_data["policies"].as_comma_string_slice().unwrap() + ); + + let _ = test_delete_api(&c, &root_token, "auth/approle/role/role1/policies", true, None); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/policies", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_policies"].as_comma_string_slice().unwrap().len(), 0); + + // RUD for secret-id-num-uses field + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/secret-id-num-uses", true); + assert!(resp.is_ok()); + + let req_data = json!({ + "secret_id_num_uses": 200, + }) + .as_object() + .unwrap() + .clone(); + let _ = + test_write_api(&c, &root_token, "auth/approle/role/role1/secret-id-num-uses", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/secret-id-num-uses", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["secret_id_num_uses"].as_int().unwrap(), req_data["secret_id_num_uses"].as_int().unwrap()); + + let _ = test_delete_api(&c, &root_token, "auth/approle/role/role1/secret-id-num-uses", true, None); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/secret-id-num-uses", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["secret_id_num_uses"].as_int().unwrap(), 0); + + // RUD for secret_id_ttl field + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/secret-id-ttl", true); + assert!(resp.is_ok()); + + let req_data = json!({ + "secret_id_ttl": 3001, + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1/secret-id-ttl", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/secret-id-ttl", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["secret_id_ttl"].as_int().unwrap(), req_data["secret_id_ttl"].as_int().unwrap()); + + let _ = test_delete_api(&c, &root_token, "auth/approle/role/role1/secret-id-ttl", true, None); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/secret-id-ttl", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["secret_id_ttl"].as_int().unwrap(), 0); + + // RUD for token-num-uses field + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-num-uses", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_num_uses"].as_int().unwrap(), 600); + + let req_data = json!({ + "token_num_uses": 60, + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1/token-num-uses", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-num-uses", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_num_uses"].as_int().unwrap(), req_data["token_num_uses"].as_int().unwrap()); + + let _ = test_delete_api(&c, &root_token, "auth/approle/role/role1/token-num-uses", true, None); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-num-uses", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_num_uses"].as_int().unwrap(), 0); + + // RUD for period field + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/period", true); + assert!(resp.is_ok()); + + let req_data = json!({ + "period": 9001, + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1/period", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/period", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["period"].as_int().unwrap(), req_data["period"].as_int().unwrap()); + + let _ = test_delete_api(&c, &root_token, "auth/approle/role/role1/period", true, None); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/period", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_period"].as_int().unwrap(), 0); + + // RUD for token_ttl field + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-ttl", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_ttl"].as_int().unwrap(), 4000); + + let req_data = json!({ + "token_ttl": 4001, + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1/token-ttl", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-ttl", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_ttl"].as_int().unwrap(), req_data["token_ttl"].as_int().unwrap()); + + let _ = test_delete_api(&c, &root_token, "auth/approle/role/role1/token-ttl", true, None); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-ttl", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_ttl"].as_int().unwrap(), 0); + + // RUD for token_max_ttl field + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-max-ttl", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_max_ttl"].as_int().unwrap(), 5000); + + let req_data = json!({ + "token_max_ttl": 5001, + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1/token-max-ttl", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-max-ttl", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_max_ttl"].as_int().unwrap(), req_data["token_max_ttl"].as_int().unwrap()); + + let _ = test_delete_api(&c, &root_token, "auth/approle/role/role1/token-max-ttl", true, None); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-max-ttl", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_max_ttl"].as_int().unwrap(), 0); + + // Delete test for role + test_delete_role(core.clone(), &root_token, "approle", "role1"); + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + assert!(resp.unwrap().is_none()); + } + + #[test] + fn test_approle_role_token_bound_cidrs_crud() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_token_bound_cidrs_crud"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let req_data = json!({ + "policies": "p,q,r,s", + "secret_id_num_uses": 10, + "secret_id_ttl": 300, + "token_ttl": 400, + "token_max_ttl": 500, + "token_num_uses": 600, + "secret_id_bound_cidrs": "127.0.0.1/32,127.0.0.1/16", + "token_bound_cidrs": "127.0.0.1/32,127.0.0.1/16", + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + + let expected = json!({ + "bind_secret_id": true, + "local_secret_ids": false, + "policies": ["p", "q", "r", "s"], + "secret_id_num_uses": 10, + "secret_id_ttl": 300, + "token_ttl": 400, + "token_max_ttl": 500, + "token_num_uses": 600, + "token_no_default_policy": false, + "secret_id_bound_cidrs": ["127.0.0.1/32", "127.0.0.1/16"], + "token_bound_cidrs": ["127.0.0.1", "127.0.0.1/16"], + "token_period": 0, + "token_explicit_max_ttl":0, + "token_policies": ["p", "q", "r", "s"], + "token_type": "default", + }); + assert_eq!(expected.as_object().unwrap().clone(), resp_data); + + let req_data = json!({ + "role_id": "test_role_id", + "policies": "a,b,c,d", + "secret_id_num_uses": 100, + "secret_id_ttl": 3000, + "token_ttl": 4000, + "token_max_ttl": 5000, + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + + let expected = json!({ + "bind_secret_id": true, + "local_secret_ids": false, + "policies": ["a", "b", "c", "d"], + "secret_id_num_uses": 100, + "secret_id_ttl": 3000, + "token_ttl": 4000, + "token_max_ttl": 5000, + "token_num_uses": 600, + "token_no_default_policy": false, + "secret_id_bound_cidrs": ["127.0.0.1/32", "127.0.0.1/16"], + "token_period": 0, + "token_explicit_max_ttl":0, + "token_bound_cidrs": ["127.0.0.1", "127.0.0.1/16"], + "token_policies": ["a", "b", "c", "d"], + "token_type": "default", + }); + assert_eq!(expected.as_object().unwrap().clone(), resp_data); + + // RUD for secret-id-bound-cidrs field + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/secret-id-bound-cidrs", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!( + resp_data["secret_id_bound_cidrs"].as_comma_string_slice().unwrap(), + expected["secret_id_bound_cidrs"].as_comma_string_slice().unwrap() + ); + + let req_data = json!({ + "secret_id_bound_cidrs": ["127.0.0.1/20"], + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api( + &c, + &root_token, + "auth/approle/role/role1/secret-id-bound-cidrs", + true, + Some(req_data.clone()), + ); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/secret-id-bound-cidrs", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!( + resp_data["secret_id_bound_cidrs"].as_comma_string_slice().unwrap(), + req_data["secret_id_bound_cidrs"].as_comma_string_slice().unwrap() + ); + + let _ = test_delete_api(&c, &root_token, "auth/approle/role/role1/secret-id-bound-cidrs", true, None); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/secret-id-bound-cidrs", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["secret_id_bound_cidrs"].as_comma_string_slice().unwrap().len(), 0); + + // RUD for token-bound-cidrs field + let expected = json!({ + "token_bound_cidrs": ["127.0.0.1", "127.0.0.1/16"], + }); + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-bound-cidrs", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!( + resp_data["token_bound_cidrs"].as_comma_string_slice().unwrap(), + expected["token_bound_cidrs"].as_comma_string_slice().unwrap() + ); + + let req_data = json!({ + "token_bound_cidrs": ["127.0.0.1/20"], + }) + .as_object() + .unwrap() + .clone(); + let _ = + test_write_api(&c, &root_token, "auth/approle/role/role1/token-bound-cidrs", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-bound-cidrs", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!( + resp_data["token_bound_cidrs"].as_comma_string_slice().unwrap(), + req_data["token_bound_cidrs"].as_comma_string_slice().unwrap() + ); + + let _ = test_delete_api(&c, &root_token, "auth/approle/role/role1/token-bound-cidrs", true, None); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1/token-bound-cidrs", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + assert_eq!(resp_data["token_bound_cidrs"].as_comma_string_slice().unwrap().len(), 0); + + // Delete test for role + test_delete_role(core.clone(), &root_token, "approle", "role1"); + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + assert!(resp.unwrap().is_none()); + } + + #[test] + fn test_approle_role_token_type_crud() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_token_type_crud"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let req_data = json!({ + "policies": "p,q,r,s", + "secret_id_num_uses": 10, + "secret_id_ttl": 300, + "token_ttl": 400, + "token_max_ttl": 500, + "token_num_uses": 600, + "token_type": "default-service", + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + + let expected = json!({ + "bind_secret_id": true, + "local_secret_ids": false, + "policies": ["p", "q", "r", "s"], + "secret_id_num_uses": 10, + "secret_id_ttl": 300, + "token_ttl": 400, + "token_max_ttl": 500, + "token_num_uses": 600, + "token_no_default_policy": false, + "secret_id_bound_cidrs": [], + "token_bound_cidrs": [], + "token_period": 0, + "token_explicit_max_ttl":0, + "token_policies": ["p", "q", "r", "s"], + "token_type": "service", + }); + assert_eq!(expected.as_object().unwrap().clone(), resp_data); + + let req_data = json!({ + "role_id": "test_role_id", + "policies": "a,b,c,d", + "secret_id_num_uses": 100, + "secret_id_ttl": 3000, + "token_ttl": 4000, + "token_max_ttl": 5000, + "token_type": "default-service", + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + + let expected = json!({ + "bind_secret_id": true, + "local_secret_ids": false, + "policies": ["a", "b", "c", "d"], + "secret_id_num_uses": 100, + "secret_id_ttl": 3000, + "token_ttl": 4000, + "token_max_ttl": 5000, + "token_num_uses": 600, + "token_no_default_policy": false, + "secret_id_bound_cidrs": [], + "token_period": 0, + "token_explicit_max_ttl":0, + "token_bound_cidrs": [], + "token_policies": ["a", "b", "c", "d"], + "token_type": "service", + }); + assert_eq!(expected.as_object().unwrap().clone(), resp_data); + + // Delete test for role + test_delete_role(core.clone(), &root_token, "approle", "role1"); + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + assert!(resp.unwrap().is_none()); + } + + #[test] + fn test_approle_role_token_util_upgrade() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_token_util_upgrade"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + // token_type missing + let req_data = json!({ + "policies": "p,q,r,s", + "secret_id_num_uses": 10, + "secret_id_ttl": 300, + "token_ttl": 400, + "token_max_ttl": 500, + "token_num_uses": 600, + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + + let expected = json!({ + "bind_secret_id": true, + "local_secret_ids": false, + "policies": ["p", "q", "r", "s"], + "secret_id_num_uses": 10, + "secret_id_ttl": 300, + "token_ttl": 400, + "token_max_ttl": 500, + "token_num_uses": 600, + "token_no_default_policy": false, + "secret_id_bound_cidrs": [], + "token_bound_cidrs": [], + "token_period": 0, + "token_explicit_max_ttl":0, + "token_policies": ["p", "q", "r", "s"], + "token_type": "default", + }); + assert_eq!(expected.as_object().unwrap().clone(), resp_data); + + // token_type empty + let req_data = json!({ + "role_id": "test_role_id", + "policies": "a,b,c,d", + "secret_id_num_uses": 100, + "secret_id_ttl": 3000, + "token_ttl": 4000, + "token_max_ttl": 5000, + "token_type": "", + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + + let expected = json!({ + "bind_secret_id": true, + "local_secret_ids": false, + "policies": ["a", "b", "c", "d"], + "secret_id_num_uses": 100, + "secret_id_ttl": 3000, + "token_ttl": 4000, + "token_max_ttl": 5000, + "token_num_uses": 600, + "token_no_default_policy": false, + "secret_id_bound_cidrs": [], + "token_period": 0, + "token_explicit_max_ttl":0, + "token_bound_cidrs": [], + "token_policies": ["a", "b", "c", "d"], + "token_type": "default", + }); + assert_eq!(expected.as_object().unwrap().clone(), resp_data); + + // token_type service + let req_data = json!({ + "role_id": "test_role_id", + "policies": "a,b,c,d", + "secret_id_num_uses": 100, + "secret_id_ttl": 3000, + "token_ttl": 4000, + "token_max_ttl": 5000, + "token_type": "service", + }) + .as_object() + .unwrap() + .clone(); + let _ = test_write_api(&c, &root_token, "auth/approle/role/role1", true, Some(req_data.clone())); + + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + + let expected = json!({ + "bind_secret_id": true, + "local_secret_ids": false, + "policies": ["a", "b", "c", "d"], + "secret_id_num_uses": 100, + "secret_id_ttl": 3000, + "token_ttl": 4000, + "token_max_ttl": 5000, + "token_num_uses": 600, + "token_no_default_policy": false, + "secret_id_bound_cidrs": [], + "token_period": 0, + "token_explicit_max_ttl":0, + "token_bound_cidrs": [], + "token_policies": ["a", "b", "c", "d"], + "token_type": "service", + }); + assert_eq!(expected.as_object().unwrap().clone(), resp_data); + + // Delete test for role + test_delete_role(core.clone(), &root_token, "approle", "role1"); + let resp = test_read_api(&c, &root_token, "auth/approle/role/role1", true); + assert!(resp.unwrap().is_none()); + } + + #[test] + fn test_approle_role_secret_id_with_ttl() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_secret_id_with_ttl"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + let mut role_data = json!({ + "policies": "default", + "secret_id_ttl": 0, + }) + .as_object() + .unwrap() + .clone(); + + let cases = vec![ + json!({"name": "zero ttl", "role_name": "role-zero-ttl", "ttl": 0, "sys_ttl_cap": false}), + json!({"name": "custom ttl", "role_name": "role-custom-ttl", "ttl": 60, "sys_ttl_cap": false}), + json!({"name": "system ttl capped", "role_name": "role-sys-ttl-cap", "ttl": 700000000, "sys_ttl_cap": true}), + ]; + + for case in cases.iter() { + let role_name = case["role_name"].as_str().unwrap(); + role_data["secret_id_ttl"] = case["ttl"].clone(); + let _ = test_write_api( + &c, + &root_token, + format!("auth/approle/role/{}", role_name).as_str(), + true, + Some(role_data.clone()), + ); + + let resp = test_write_api( + &c, + &root_token, + format!("auth/approle/role/{}/secret-id", role_name).as_str(), + true, + None, + ); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let secret_id_ttl = resp_data["secret_id_ttl"].as_duration().unwrap(); + if case["sys_ttl_cap"].as_bool().unwrap() { + assert_eq!(secret_id_ttl, MAX_LEASE_DURATION_SECS); + } else { + assert_eq!(secret_id_ttl, case["ttl"].as_duration().unwrap()); + } + } + } + + #[test] + fn test_approle_role_secret_id_accessor_cross_delete() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_role_delete_secret_id_accessor_cross_delete"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + let c = core.read().unwrap(); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + + // Create First Role + test_write_role(core.clone(), &root_token, "approle", "role1", "", "a,b", true); + let _ = generate_secret_id(core.clone(), &root_token, "approle", "role1"); + + // Create Second Role + test_write_role(core.clone(), &root_token, "approle", "role2", "", "a,b", true); + let _ = generate_secret_id(core.clone(), &root_token, "approle", "role2"); + + // Get role2 secretID Accessor + let resp = test_list_api(&c, &root_token, "auth/approle/role/role2/secret-id", true); + assert!(resp.is_ok()); + let resp_data = resp.unwrap().unwrap().data.unwrap(); + let keys = resp_data["keys"].as_array().unwrap(); + assert_eq!(keys.len(), 1); + + // Attempt to destroy role2 secretID accessor using role1 path + + let hmac_secret_id = keys[0].as_str().unwrap(); + let hmac_data = json!({ + "secret_id_accessor": hmac_secret_id, + }) + .as_object() + .unwrap() + .clone(); + let _ = test_delete_api( + &c, + &root_token, + "auth/approle/role/role1/secret-id-accessor/destroy", + false, + Some(hmac_data.clone()), + ); + } +} diff --git a/src/modules/credential/approle/path_tidy_secret_id.rs b/src/modules/credential/approle/path_tidy_secret_id.rs new file mode 100644 index 0000000..7d91bb4 --- /dev/null +++ b/src/modules/credential/approle/path_tidy_secret_id.rs @@ -0,0 +1,588 @@ +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }, + time::SystemTime, +}; + +use go_defer::defer; + +use super::{ + validation::SecretIdAccessorStorageEntry, AppRoleBackend, AppRoleBackendInner, SECRET_ID_ACCESSOR_LOCAL_PREFIX, + SECRET_ID_ACCESSOR_PREFIX, SECRET_ID_LOCAL_PREFIX, SECRET_ID_PREFIX, +}; +use crate::{ + context::Context, + errors::RvError, + logical::{Backend, Operation, Path, PathOperation, Request, Response, CTX_KEY_BACKEND_PATH}, + new_path, new_path_internal, + storage::Storage, +}; + +pub const CTX_KEY_BACKEND_PATH_INNER: &str = "backend.path.inner"; + +impl AppRoleBackend { + pub fn tidy_secret_id_path(&self) -> Path { + let approle_backend_ref1 = Arc::clone(&self.inner); + let approle_backend_ref2 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"tidy/secret-id$", + operations: [ + {op: Operation::Write, handler: approle_backend_ref1.tidy_secret_id} + ], + help: r#" +SecretIDs will have expiration time attached to them. The periodic function +of the backend will look for expired entries and delete them. This happens once in a minute. Invoking +this endpoint will trigger the clean-up action, without waiting for the backend's periodic function. +"# + }); + + path.ctx.set(CTX_KEY_BACKEND_PATH_INNER, approle_backend_ref2); + + path + } +} + +impl AppRoleBackendInner { + async fn tidy_secret_id_routine(&self, storage: Arc) { + let check_count = AtomicU32::new(0); + + defer! ( + self.tidy_secret_id_cas_guard.store(0, Ordering::SeqCst); + log::info!("done checking entries, num_entries: {}", check_count.load(Ordering::SeqCst)); + ); + + let salt = self.salt.read(); + if salt.is_err() { + log::error!("error tidying secret IDs, err: {}", salt.unwrap_err()); + return; + } + + let salt = salt.unwrap(); + + let tidy_func = |secret_id_prefix_to_use: &str, accessor_id_prefix_to_use: &str| -> Result<(), RvError> { + log::info!("listing accessors, prefix: {}", accessor_id_prefix_to_use); + // List all the accessors and add them all to a map + // These hashes are the result of salting the accessor id. + let accessor_hashes = storage.list(accessor_id_prefix_to_use)?; + let mut skip_hashes: HashMap = HashMap::new(); + let mut accessor_entry_by_hash: HashMap = HashMap::new(); + for accessor_hash in accessor_hashes.iter() { + let entry_index = format!("{}{}", accessor_id_prefix_to_use, accessor_hash); + let storage_entry = storage.get(&entry_index)?; + if storage_entry.is_none() { + continue; + } + + let entry = storage_entry.unwrap(); + let ret: SecretIdAccessorStorageEntry = serde_json::from_slice(entry.value.as_slice())?; + accessor_entry_by_hash.insert(accessor_hash.clone(), ret); + } + + let mut secret_id_cleanup_func = |secret_id_hmac: &str, + role_name_hmac: &str, + secret_id_prefix_to_use: &str| + -> Result<(), RvError> { + check_count.fetch_add(1, Ordering::SeqCst); + + let s = Arc::as_ref(&storage); + + let lock_entry = self.secret_id_locks.get_lock(secret_id_hmac); + let _locked = lock_entry.lock.write()?; + + let secret_id_storage_entry = self + .get_secret_id_storage_entry(s, secret_id_prefix_to_use, role_name_hmac, secret_id_hmac)? + .ok_or(RvError::ErrResponse(format!( + "entry for secret id was nil, secret_id_hmac: {}", + secret_id_hmac + )))?; + + // If a secret ID entry does not have a corresponding accessor + // entry, revoke the secret ID immediately + if self + .get_secret_id_accessor_entry( + s, + &secret_id_storage_entry.secret_id_accessor, + secret_id_prefix_to_use, + )? + .is_none() + { + self.delete_secret_id_storage_entry(s, secret_id_prefix_to_use, role_name_hmac, secret_id_hmac)?; + return Ok(()); + } + + // ExpirationTime not being set indicates non-expiring SecretIDs + if SystemTime::now() > secret_id_storage_entry.expiration_time { + log::info!("found expired secret ID"); + // Clean up the accessor of the secret ID first + self.delete_secret_id_accessor_entry( + s, + &secret_id_storage_entry.secret_id_accessor, + secret_id_prefix_to_use, + )?; + + self.delete_secret_id_storage_entry(s, secret_id_prefix_to_use, role_name_hmac, secret_id_hmac)?; + + return Ok(()); + } + + // At this point, the secret ID is not expired and is valid. Flag + // the corresponding accessor as not needing attention. + let salt_id = salt.as_ref().unwrap().salt_id(&secret_id_storage_entry.secret_id_accessor)?; + skip_hashes.insert(salt_id, true); + + Ok(()) + }; + + log::info!("listing role HMACs, prefix: {}", secret_id_prefix_to_use); + + let role_name_hmacs = storage.list(secret_id_prefix_to_use)?; + for role_name_hmac in role_name_hmacs.iter() { + log::info!("listing secret id HMACs, role_hame: {}", role_name_hmac); + let key = format!("{}{}/", secret_id_prefix_to_use, role_name_hmac); + let secret_id_hmacs = storage.list(&key)?; + for secret_id_hmac in secret_id_hmacs.iter() { + secret_id_cleanup_func(&secret_id_hmac, &role_name_hmac, secret_id_prefix_to_use)?; + } + } + + if accessor_hashes.len() > skip_hashes.len() { + // There is some raciness here because we're querying secretids for + // roles without having a lock while doing so. Because + // accessor_entry_by_hash was populated previously, at worst this may + // mean that we fail to clean up something we ought to. + let mut all_secret_id_hmacs: HashMap = HashMap::new(); + for role_name_hmac in role_name_hmacs.iter() { + let key = format!("{}{}/", secret_id_prefix_to_use, role_name_hmac); + let secret_id_hmacs = storage.list(&key)?; + for secret_id_hmac in secret_id_hmacs.iter() { + all_secret_id_hmacs.insert(secret_id_hmac.clone(), true); + } + } + + for (accessor_hash, accessor_entry) in accessor_entry_by_hash.iter() { + let lock_entry = self.secret_id_locks.get_lock(&accessor_entry.secret_id_hmac); + let _locked = lock_entry.lock.write()?; + + // Don't clean up accessor index entry if secretid cleanup func + // determined that it should stay. + if skip_hashes.contains_key(accessor_hash) { + continue; + } + + // Don't clean up accessor index entry if referenced in role. + if all_secret_id_hmacs.contains_key(&accessor_entry.secret_id_hmac) { + continue; + } + + let entry_index = format!("{}{}", accessor_id_prefix_to_use, accessor_hash); + + storage.delete(&entry_index)?; + } + } + + Ok(()) + }; + + if let Err(err) = tidy_func(SECRET_ID_PREFIX, SECRET_ID_ACCESSOR_PREFIX) { + log::error!("error tidying global secret IDs, error: {}", err); + return; + } + + if let Err(err) = tidy_func(SECRET_ID_LOCAL_PREFIX, SECRET_ID_ACCESSOR_LOCAL_PREFIX) { + log::error!("error tidying local secret IDs, error: {}", err); + return; + } + } + + pub fn tidy_secret_id(&self, backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let mut resp = Response::new(); + if self.tidy_secret_id_cas_guard.compare_exchange(0, 1, Ordering::SeqCst, Ordering::SeqCst).is_err() { + resp.add_warning("Tidy operation already in progress"); + return Ok(Some(resp)); + } + + let storage = Arc::clone(req.storage.as_ref().unwrap()); + + let ctx = backend.get_ctx().ok_or(RvError::ErrRequestInvalid)?; + let path: Arc = ctx + .get(CTX_KEY_BACKEND_PATH) + .ok_or(RvError::ErrRequestInvalid)? + .downcast::() + .map_err(|_| RvError::ErrRequestInvalid)? + .clone(); + let path_inner: Arc = path + .ctx + .get(CTX_KEY_BACKEND_PATH_INNER) + .ok_or(RvError::ErrRequestInvalid)? + .downcast::() + .map_err(|_| RvError::ErrRequestInvalid)? + .clone(); + + let task = actix_rt::spawn(async move { + path_inner.tidy_secret_id_routine(storage).await; + }); + + req.add_task(task); + + resp.set_request_id(&req.id); + resp.add_warning( + "Tidy operation successfully started. Any information from the operation will be printed to RustyVault's \ + server logs.", + ); + + let ret = Response::respond_with_status_code(Some(resp), 202); + + Ok(Some(ret)) + } +} + +#[cfg(test)] +mod test { + use std::{ + collections::HashMap, + default::Default, + env, fs, + sync::{Arc, Mutex, RwLock}, + thread, + time::{Duration, Instant}, + }; + + use actix_rt::System; + use as_any::Downcast; + use go_defer::defer; + use serde_json::{json, Map, Value}; + use tokio::runtime::Builder; + + use super::{ + super::{path_role::RoleEntry, AppRoleModule}, + *, + }; + use crate::{ + core::{Core, SealConfig}, + logical::{Operation, Request}, + storage::{self, Storage, StorageEntry}, + }; + + fn test_write_api( + core: &Core, + token: &str, + path: &str, + is_ok: bool, + data: Option>, + ) -> Result, RvError> { + let mut req = Request::new(path); + req.operation = Operation::Write; + req.client_token = token.to_string(); + req.body = data; + + let resp = core.handle_request(&mut req); + println!("write resp: {:?}", resp); + assert_eq!(resp.is_ok(), is_ok); + resp + } + + fn mount_approle_auth(core: Arc>, token: &str, path: &str) { + let core = core.read().unwrap(); + + let auth_data = json!({ + "type": "approle", + }) + .as_object() + .unwrap() + .clone(); + + let resp = test_write_api(&core, token, format!("sys/auth/{}", path).as_str(), true, Some(auth_data)); + assert!(resp.is_ok()); + } + + fn rusty_vault_init(dir: &str) -> (String, Arc>) { + let root_token; + + println!("rusty_vault_init, dir: {}", dir); + + let mut conf: HashMap = HashMap::new(); + conf.insert("path".to_string(), Value::String(dir.to_string())); + + let backend = storage::new_backend("file", &conf).unwrap(); + + let barrier = storage::barrier_aes_gcm::AESGCMBarrier::new(Arc::clone(&backend)); + + let c = Arc::new(RwLock::new(Core { physical: backend, barrier: Arc::new(barrier), ..Default::default() })); + + { + let mut core = c.write().unwrap(); + assert!(core.config(Arc::clone(&c), None).is_ok()); + + let seal_config = SealConfig { secret_shares: 10, secret_threshold: 5 }; + + let result = core.init(&seal_config); + assert!(result.is_ok()); + let init_result = result.unwrap(); + println!("init_result: {:?}", init_result); + + let mut unsealed = false; + for i in 0..seal_config.secret_threshold { + let key = &init_result.secret_shares[i as usize]; + let unseal = core.unseal(key); + assert!(unseal.is_ok()); + unsealed = unseal.unwrap(); + } + + root_token = init_result.root_token; + println!("root_token: {:?}", root_token); + + assert!(unsealed); + } + + (root_token, c) + } + + #[test] + fn test_approle_tidy_dangling_accessors_normal() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_tidy_dangling_accessors_normal"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + let c = core.read().unwrap(); + + let module = c.module_manager.get_module("approle").unwrap(); + let approle_mod = module.read().unwrap(); + let approle_module = approle_mod.as_ref().downcast_ref::().unwrap(); + + // Create a role + let mut req = Request::new("/auth/approle/role1"); + req.operation = Operation::Write; + req.storage = c.get_system_view().map(|arc| arc as Arc); + + let role_entry = RoleEntry { + role_id: "testroleid".to_string(), + hmac_key: "testhmackey".to_string(), + bind_secret_id: true, + secret_id_ttl: Duration::from_secs(300), + policies: vec!["a".to_string(), "b".to_string(), "c".to_string()], + ..Default::default() + }; + let resp = approle_module.set_role(&mut req, "role1", &role_entry, ""); + assert!(resp.is_ok()); + + // Create a secret-id + req.operation = Operation::Write; + req.path = "auth/approle/role/role1/secret-id".to_string(); + req.client_token = root_token.to_string(); + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + + let mut mock_backend = approle_module.new_backend(); + assert!(mock_backend.init().is_ok()); + + let resp = approle_module.write_role_secret_id(&mock_backend, &mut req); + assert!(resp.is_ok()); + + let accessor = req.storage_list("accessor/"); + assert!(accessor.is_ok()); + + let accessor = accessor.unwrap(); + assert_eq!(accessor.len(), 1); + + let entry = StorageEntry::new( + "accessor/invalid1", + &SecretIdAccessorStorageEntry { secret_id_hmac: "samplesecretidhmac".to_string() }, + ) + .unwrap(); + + assert!(req.storage_put(&entry).is_ok()); + + let entry = StorageEntry::new( + "accessor/invalid2", + &SecretIdAccessorStorageEntry { secret_id_hmac: "samplesecretidhmac2".to_string() }, + ) + .unwrap(); + + assert!(req.storage_put(&entry).is_ok()); + + let accessor = req.storage_list("accessor/"); + assert!(accessor.is_ok()); + let accessor = accessor.unwrap(); + assert_eq!(accessor.len(), 3); + + let rt = + System::with_tokio_rt(|| Builder::new_multi_thread().worker_threads(128).enable_all().build().unwrap()); + rt.block_on(async { + req.operation = Operation::Write; + req.path = "tidy/secret-id".to_string(); + let _resp = mock_backend.handle_request(&mut req); + actix_rt::System::current().stop(); + }); + + let _ = rt.run().unwrap(); + + std::thread::sleep(Duration::from_secs(5)); + + let accessor = req.storage_list("accessor/"); + assert!(accessor.is_ok()); + let accessor = accessor.unwrap(); + assert_eq!(accessor.len(), 1); + } + + #[test] + fn test_approle_tidy_dangling_accessors_race() { + let dir = env::temp_dir().join("rusty_vault_credential_approle_tidy_dangling_accessors_race"); + let _ = fs::remove_dir_all(&dir); + assert!(fs::create_dir(&dir).is_ok()); + defer! ( + assert!(fs::remove_dir_all(&dir).is_ok()); + ); + + let (root_token, core) = rusty_vault_init(dir.to_string_lossy().into_owned().as_str()); + + // Mount approle auth to path: auth/approle + mount_approle_auth(core.clone(), &root_token, "approle"); + let c = core.read().unwrap(); + + let module = c.module_manager.get_module("approle").unwrap(); + let approle_mod = module.read().unwrap(); + let approle_module = approle_mod.as_ref().downcast_ref::().unwrap(); + + let mut mock_backend = approle_module.new_backend(); + assert!(mock_backend.init().is_ok()); + + // Create a role + let mut req = Request::new("/auth/approle/role1"); + req.operation = Operation::Write; + req.storage = c.get_system_view().map(|arc| arc as Arc); + + let role_entry = RoleEntry { + role_id: "testroleid".to_string(), + hmac_key: "testhmackey".to_string(), + bind_secret_id: true, + secret_id_ttl: Duration::from_secs(300), + policies: vec!["a".to_string(), "b".to_string(), "c".to_string()], + ..Default::default() + }; + let resp = approle_module.set_role(&mut req, "role1", &role_entry, ""); + assert!(resp.is_ok()); + + // Create a secret-id + req.operation = Operation::Write; + req.path = "auth/approle/role/role1/secret-id".to_string(); + req.client_token = root_token.to_string(); + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + let resp = approle_module.write_role_secret_id(&mock_backend, &mut req); + assert!(resp.is_ok()); + + let count = Arc::new(Mutex::new(1)); + let start = Instant::now(); + let core_cloned = core.clone(); + + //let invalid_accessors = Arc::new(Mutex::new()); + + let rt = + System::with_tokio_rt(|| Builder::new_multi_thread().worker_threads(128).enable_all().build().unwrap()); + + rt.block_on(async { + while start.elapsed() < Duration::new(5, 0) { + if start.elapsed() > Duration::from_millis(100) + && approle_module.tidy_secret_id_cas_guard.load(Ordering::SeqCst) == 0 + { + req.operation = Operation::Write; + req.path = "tidy/secret-id".to_string(); + let _ = mock_backend.handle_request(&mut req); + } + + let core_cloned2 = core_cloned.clone(); + let token = root_token.clone(); + let mb = mock_backend.clone(); + + actix_rt::spawn(async move { + let c = core_cloned2.read().unwrap(); + let module = c.module_manager.get_module("approle").unwrap(); + let approle_mod = module.read().unwrap(); + let approle_module = approle_mod.as_ref().downcast_ref::().unwrap(); + let mut req = Request::new("auth/approle/role/role1/secret-id"); + req.operation = Operation::Write; + req.client_token = token.clone(); + let _resp = c.handle_request(&mut req); + req.storage = c.get_system_view().map(|arc| arc as Arc); + let resp = approle_module.write_role_secret_id(&mb, &mut req); + assert!(resp.is_ok()); + }); + + let mut num = count.lock().unwrap(); + + let entry = StorageEntry::new( + format!("accessor/invalid{}", *num).as_str(), + &SecretIdAccessorStorageEntry { secret_id_hmac: "samplesecretidhmac".to_string() }, + ) + .unwrap(); + + assert!(req.storage_put(&entry).is_ok()); + + *num += 1; + + thread::sleep(Duration::from_micros(10)); + } + + for task in &mut req.tasks { + task.await.unwrap(); + } + + actix_rt::System::current().stop(); + }); + + let _ = rt.run().unwrap(); + + // Wait for tidy to finish + while approle_module.tidy_secret_id_cas_guard.load(Ordering::SeqCst) != 0 { + thread::sleep(Duration::from_micros(100)); + } + + // Run tidy again + req.tasks.clear(); + + let rt = + System::with_tokio_rt(|| Builder::new_multi_thread().worker_threads(128).enable_all().build().unwrap()); + rt.block_on(async { + req.operation = Operation::Write; + req.path = "tidy/secret-id".to_string(); + let resp = mock_backend.handle_request(&mut req); + assert!(resp.is_ok()); + + for task in &mut req.tasks { + task.await.unwrap(); + } + + actix_rt::System::current().stop(); + }); + + let _ = rt.run().unwrap(); + + let num = count.lock().unwrap(); + + let accessor = req.storage_list("accessor/"); + assert!(accessor.is_ok()); + let accessor = accessor.unwrap(); + assert_eq!(accessor.len(), *num); + + let role_hmacs = req.storage_list(SECRET_ID_PREFIX); + assert!(role_hmacs.is_ok()); + let role_hmacs = role_hmacs.unwrap(); + assert_eq!(role_hmacs.len(), 1); + + let secret_ids = req.storage_list(format!("{}{}", SECRET_ID_PREFIX, role_hmacs[0]).as_str()); + assert!(secret_ids.is_ok()); + let secret_ids = secret_ids.unwrap(); + assert_eq!(secret_ids.len(), *num); + } +} diff --git a/src/modules/credential/approle/validation.rs b/src/modules/credential/approle/validation.rs new file mode 100644 index 0000000..53052f0 --- /dev/null +++ b/src/modules/credential/approle/validation.rs @@ -0,0 +1,418 @@ +//! This module is a Rust replica of +//! https://github.com/hashicorp/vault/blob/main/builtin/credential/approle/validation.go + +use std::{ + collections::HashMap, + time::{Duration, SystemTime}, +}; + +use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer}; +use serde::{Deserialize, Serialize}; + +use super::{AppRoleBackendInner, SECRET_ID_ACCESSOR_LOCAL_PREFIX, SECRET_ID_ACCESSOR_PREFIX, SECRET_ID_LOCAL_PREFIX}; +use crate::{ + errors::RvError, + modules::auth::expiration::MAX_LEASE_DURATION_SECS, + storage::{Storage, StorageEntry}, + utils::{self, deserialize_duration, deserialize_system_time, serialize_duration, serialize_system_time}, +}; + +const MAX_HMAC_INPUT_LENGTH: usize = 4096; + +// secretIDStorageEntry represents the information stored in storage +// when a secret_id is created. The structure of the secret_id storage +// entry is the same for all the types of secret_ids generated. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SecretIdStorageEntry { + // Accessor for the secret_id. It is a random uuid serving as + // a secondary index for the secret_id. This uniquely identifies + // the secret_id it belongs to, and hence can be used for listing + // and deleting secret_ids. Accessors cannot be used as valid + // secret_id during login. + pub secret_id_accessor: String, + + // Number of times this secret_id can be used to perform the login + // operation + pub secret_id_num_uses: i64, + + // Duration after which this secret_id should expire. This is capped by + // the backend mount's max TTL value. + #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] + pub secret_id_ttl: Duration, + + // The time when the secret_id was created + #[serde(serialize_with = "serialize_system_time", deserialize_with = "deserialize_system_time")] + pub creation_time: SystemTime, + + // The time when the secret_id becomes eligible for tidy operation. + // Tidying is performed by the PeriodicFunc of the backend which is 1 + // minute apart. + #[serde(serialize_with = "serialize_system_time", deserialize_with = "deserialize_system_time")] + pub expiration_time: SystemTime, + + // The time representing the last time this storage entry was modified + #[serde(serialize_with = "serialize_system_time", deserialize_with = "deserialize_system_time")] + pub last_updated_time: SystemTime, + + // metadata that belongs to the secret_id + pub metadata: HashMap, + + // cidr_list is a set of CIDR blocks that impose source address + // restrictions on the usage of secret_id + pub cidr_list: Vec, + + // token_cidr_list is a set of CIDR blocks that impose source address + // restrictions on the usage of the token generated by this secret_id + pub token_cidr_list: Vec, +} + +// Represents the payload of the storage entry of the accessor that maps to a +// unique secret_id. Note that secret_id should never be stored in plaintext +// anywhere in the backend. secret_id_hmac will be used as an index to fetch the +// properties of the secret_id and to delete the secret_id. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SecretIdAccessorStorageEntry { + // Hash of the secret_id which can be used to find the storage index at which + // properties of secret_id is stored. + pub secret_id_hmac: String, +} + +impl Default for SecretIdStorageEntry { + fn default() -> Self { + Self { + secret_id_accessor: String::new(), + secret_id_num_uses: 0, + secret_id_ttl: Duration::from_secs(0), + creation_time: SystemTime::now(), + expiration_time: SystemTime::now(), + last_updated_time: SystemTime::now(), + metadata: HashMap::new(), + cidr_list: Vec::new(), + token_cidr_list: Vec::new(), + } + } +} + +impl Default for SecretIdAccessorStorageEntry { + fn default() -> Self { + Self { secret_id_hmac: String::new() } + } +} + +impl AppRoleBackendInner { + // get_secret_id_storage_entry fetches the secret ID properties from physical + // storage. The entry will be indexed based on the given HMACs of both role + // name and the secret ID. This method will not acquire secret ID lock to fetch + // the storage entry. Locks need to be acquired before calling this method. + pub fn get_secret_id_storage_entry( + &self, + storage: &dyn Storage, + role_secret_id_prefix: &str, + role_name_hmac: &str, + secret_id_hmac: &str, + ) -> Result, RvError> { + if secret_id_hmac == "" { + return Err(RvError::ErrResponse("missing secret id hmac".to_string())); + } + + if role_name_hmac == "" { + return Err(RvError::ErrResponse("missing role name hmac".to_string())); + } + + let entry_index = format!("{}{}/{}", role_secret_id_prefix, role_name_hmac, secret_id_hmac); + let storage_entry = storage.get(&entry_index)?; + if storage_entry.is_none() { + return Ok(None); + } + + let entry = storage_entry.unwrap(); + let ret: SecretIdStorageEntry = serde_json::from_slice(entry.value.as_slice())?; + + Ok(Some(ret)) + } + + // set_secret_id_storage_entry creates or updates a secret ID entry at the + // physical storage. The entry will be indexed based on the given HMACs of both + // role name and the secret ID. This method will not acquire secret ID lock to + // create/update the storage entry. Locks need to be acquired before calling + // this method. + pub fn set_secret_id_storage_entry( + &self, + storage: &dyn Storage, + role_secret_id_prefix: &str, + role_name_hmac: &str, + secret_id_hmac: &str, + secret_entry: &SecretIdStorageEntry, + ) -> Result<(), RvError> { + if role_secret_id_prefix == "" { + return Err(RvError::ErrResponse("missing secret id prefix".to_string())); + } + + if secret_id_hmac == "" { + return Err(RvError::ErrResponse("missing secret id hmac".to_string())); + } + + if role_name_hmac == "" { + return Err(RvError::ErrResponse("missing role name hmac".to_string())); + } + + let entry_index = format!("{}{}/{}", role_secret_id_prefix, role_name_hmac, secret_id_hmac); + let entry = StorageEntry::new(&entry_index, secret_entry)?; + + storage.put(&entry) + } + + pub fn delete_secret_id_storage_entry( + &self, + storage: &dyn Storage, + role_secret_id_prefix: &str, + role_name_hmac: &str, + secret_id_hmac: &str, + ) -> Result<(), RvError> { + if secret_id_hmac == "" { + return Err(RvError::ErrResponse("missing secret id hmac".to_string())); + } + + if role_name_hmac == "" { + return Err(RvError::ErrResponse("missing role name hmac".to_string())); + } + + let entry_index = format!("{}{}/{}", role_secret_id_prefix, role_name_hmac, secret_id_hmac); + storage.delete(&entry_index) + } + + // register_secret_id_entry creates a new storage entry for the given secret_id. + pub fn register_secret_id_entry( + &self, + storage: &dyn Storage, + role_name: &str, + secret_id: &str, + hmac_key: &str, + role_secret_id_prefix: &str, + secret_entry: &mut SecretIdStorageEntry, + ) -> Result<(), RvError> { + let role_name_hmac = create_hmac(hmac_key, role_name)?; + let secret_id_hmac = create_hmac(hmac_key, secret_id)?; + + let lock_entry = self.secret_id_locks.get_lock(&secret_id_hmac); + { + let _locked = lock_entry.lock.read()?; + + let entry = + self.get_secret_id_storage_entry(storage, role_secret_id_prefix, &role_name_hmac, &secret_id_hmac)?; + if entry.is_some() { + return Err(RvError::ErrResponse("secret_id is already registered".to_string())); + } + } + { + let _locked = lock_entry.lock.write()?; + + let entry = + self.get_secret_id_storage_entry(storage, role_secret_id_prefix, &role_name_hmac, &secret_id_hmac)?; + if entry.is_some() { + return Err(RvError::ErrResponse("secret_id is already registered".to_string())); + } + + let now = SystemTime::now(); + secret_entry.creation_time = now; + secret_entry.last_updated_time = now; + + let ttl = self.derive_secret_id_ttl(secret_entry.secret_id_ttl); + if ttl.as_secs() != 0 { + secret_entry.expiration_time = now + ttl; + } + + self.create_secret_id_accessor_entry(storage, secret_entry, &secret_id_hmac, &role_secret_id_prefix)?; + + self.set_secret_id_storage_entry( + storage, + role_secret_id_prefix, + &role_name_hmac, + &secret_id_hmac, + secret_entry, + )?; + Ok(()) + } + } + + // derive_secret_id_ttl determines the secret id TTL to use based on the system's + // max lease TTL. + // + // If secret_id_ttl is negative or if it crosses the backend mount's limit, + // return to backend's max lease TTL. Otherwise, return the provided secret_id_ttl + // value. + pub fn derive_secret_id_ttl(&self, secret_id_ttl: Duration) -> Duration { + if secret_id_ttl > MAX_LEASE_DURATION_SECS { + return MAX_LEASE_DURATION_SECS; + } + + return secret_id_ttl; + } + + // secret_id_accessor_entry is used to read the storage entry that maps an + // accessor to a secret_id. + pub fn get_secret_id_accessor_entry( + &self, + storage: &dyn Storage, + secret_id_accessor: &str, + role_secret_id_prefix: &str, + ) -> Result, RvError> { + if secret_id_accessor == "" { + return Err(RvError::ErrResponse("missing secret id accessor".to_string())); + } + + let salt = self.salt.read()?; + if salt.is_none() { + return Err(RvError::ErrResponse("approle module not initialized".to_string())); + } + + let salt_id = salt.as_ref().unwrap().salt_id(secret_id_accessor)?; + + let mut accessor_prefix = SECRET_ID_ACCESSOR_PREFIX; + if role_secret_id_prefix == SECRET_ID_LOCAL_PREFIX { + accessor_prefix = SECRET_ID_ACCESSOR_LOCAL_PREFIX; + } + + let entry_index = format!("{}{}", accessor_prefix, salt_id); + + let lock_entry = self.secret_id_accessor_locks.get_lock(&secret_id_accessor); + let _locked = lock_entry.lock.read()?; + + let storage_entry = storage.get(&entry_index)?; + if storage_entry.is_none() { + return Ok(None); + } + + let entry = storage_entry.unwrap(); + let ret: SecretIdAccessorStorageEntry = serde_json::from_slice(entry.value.as_slice())?; + + Ok(Some(ret)) + } + + // create_secret_id_accessor_entry creates an identifier for the secret_id. + // A storage index, mapping the accessor to the secret_id is also created. + // This method should be called when the lock for the corresponding secret_id is held. + pub fn create_secret_id_accessor_entry( + &self, + storage: &dyn Storage, + entry: &mut SecretIdStorageEntry, + secret_id_hmac: &str, + role_secret_id_prefix: &str, + ) -> Result<(), RvError> { + entry.secret_id_accessor = utils::generate_uuid(); + + let salt = self.salt.read()?; + if salt.is_none() { + return Err(RvError::ErrResponse("approle module not initialized".to_string())); + } + + let salt_id = salt.as_ref().unwrap().salt_id(&entry.secret_id_accessor)?; + + let mut accessor_prefix = SECRET_ID_ACCESSOR_PREFIX; + if role_secret_id_prefix == SECRET_ID_LOCAL_PREFIX { + accessor_prefix = SECRET_ID_ACCESSOR_LOCAL_PREFIX; + } + + let entry_index = format!("{}{}", accessor_prefix, salt_id); + + let lock_entry = self.secret_id_accessor_locks.get_lock(&entry.secret_id_accessor); + let _locked = lock_entry.lock.write()?; + + let entry = StorageEntry::new( + &entry_index, + &SecretIdAccessorStorageEntry { secret_id_hmac: secret_id_hmac.to_string() }, + )?; + + storage.put(&entry) + } + + // delete_secret_id_accessor_entry deletes the storage index mapping the accessor to a secret_id. + pub fn delete_secret_id_accessor_entry( + &self, + storage: &dyn Storage, + secret_id_accessor: &str, + role_secret_id_prefix: &str, + ) -> Result<(), RvError> { + let salt = self.salt.read()?; + if salt.is_none() { + return Err(RvError::ErrResponse("approle module not initialized".to_string())); + } + + let salt_id = salt.as_ref().unwrap().salt_id(secret_id_accessor)?; + + let mut accessor_prefix = SECRET_ID_ACCESSOR_PREFIX; + if role_secret_id_prefix == SECRET_ID_LOCAL_PREFIX { + accessor_prefix = SECRET_ID_ACCESSOR_LOCAL_PREFIX; + } + + let entry_index = format!("{}{}", accessor_prefix, salt_id); + + let lock_entry = self.secret_id_accessor_locks.get_lock(secret_id_accessor); + let _locked = lock_entry.lock.write()?; + + storage.delete(&entry_index) + } + + // flush_role_secrets deletes all the secret_id that belong to the given + // role_id. + pub fn flush_role_secrets( + &self, + storage: &dyn Storage, + role_name: &str, + hmac_key: &str, + role_secret_id_prefix: &str, + ) -> Result<(), RvError> { + let role_name_hmac = create_hmac(hmac_key, role_name)?; + let key = format!("{}{}/", role_secret_id_prefix, role_name_hmac); + let secret_id_hmacs = storage.list(&key)?; + for secret_id_hmac in secret_id_hmacs.iter() { + let entry_index = format!("{}{}/{}", role_secret_id_prefix, role_name_hmac, secret_id_hmac); + let lock_entry = self.secret_id_locks.get_lock(&secret_id_hmac); + let _locked = lock_entry.lock.write()?; + storage.delete(&entry_index)? + } + + Ok(()) + } +} + +pub fn create_hmac(key: &str, value: &str) -> Result { + if key == "" { + return Err(RvError::ErrResponse("invalid hmac key".to_string())); + } + + if value.len() > MAX_HMAC_INPUT_LENGTH { + return Err(RvError::ErrResponse(format!("value is longer than maximum of {} bytes", MAX_HMAC_INPUT_LENGTH))); + } + + let pkey = PKey::hmac(key.as_bytes())?; + let mut signer = Signer::new(MessageDigest::sha256(), &pkey)?; + signer.update(value.as_bytes())?; + let hmac = signer.sign_to_vec()?; + Ok(hex::encode(hmac.as_slice())) +} + +pub fn verify_cidr_role_secret_id_subset( + secret_id_cidrs: &[String], + role_bound_cidr_list: &[String], +) -> Result<(), RvError> { + if !secret_id_cidrs.is_empty() && !role_bound_cidr_list.is_empty() { + let cidr_list: Vec = role_bound_cidr_list + .iter() + .map(|cidr| if cidr.contains("/") { cidr.clone() } else { format!("{}/32", cidr) }) + .collect(); + + let cidr_list_ref: Vec<&str> = cidr_list.iter().map(String::as_str).collect(); + let cidrs_ref: Vec<&str> = secret_id_cidrs.iter().map(AsRef::as_ref).collect(); + + if !utils::cidr::subset_blocks(&cidr_list_ref, &cidrs_ref)? { + return Err(RvError::ErrResponse(format!( + "failed to verify subset relationship between CIDR blocks on the role {:?} and CIDR blocks on the \ + secret ID {:?}", + cidr_list_ref, cidrs_ref + ))); + } + } + + Ok(()) +} diff --git a/src/modules/credential/mod.rs b/src/modules/credential/mod.rs index 36cb184..5e7bf01 100644 --- a/src/modules/credential/mod.rs +++ b/src/modules/credential/mod.rs @@ -2,5 +2,5 @@ //! , etc. //! +pub mod approle; pub mod userpass; -//pub mod cert; diff --git a/src/utils/token_util.rs b/src/utils/token_util.rs index 7596584..eef99ab 100644 --- a/src/utils/token_util.rs +++ b/src/utils/token_util.rs @@ -123,7 +123,6 @@ impl TokenParams { self.token_num_uses = num_uses_value.as_u64().ok_or(RvError::ErrRequestFieldInvalid)?; } - println!("111"); if let Ok(type_value) = req.get_data_or_default("token_type") { let token_type = type_value.as_str().ok_or(RvError::ErrRequestFieldInvalid)?.to_string(); self.token_type = match token_type.as_str() { @@ -140,7 +139,6 @@ impl TokenParams { } }; } - println!("222"); if let Ok(policies_value) = req.get_data("token_policies") { self.token_policies = policies_value.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?;