diff --git a/src/apikeys.rs b/src/apikeys.rs index d139b351a..8dc1c1c6e 100644 --- a/src/apikeys.rs +++ b/src/apikeys.rs @@ -30,12 +30,43 @@ use serde::Deserialize; /// Request body for creating a new API key. `roles` is a set of role names /// that must already exist in the tenant; permissions for the backing user /// are derived from these roles (same flow as native/OAuth users). +/// `apiKey` lets a caller register their own key; omitting it preserves +/// the existing server-generated key behavior. +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ApiKeyType { + Ingestor, + Query, +} + #[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct CreateApiKeyRequest { pub key_name: String, #[serde(default)] pub roles: HashSet, + pub api_key: Option, + /// Omit for the existing UI/API flow that assigns explicit roles. + pub api_key_type: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PatchApiKeyRequest { + pub key_name: Option, + pub api_key: Option, +} + +/// Require a hyphenated UUID v4, allowing either letter case. +pub fn validate_api_key(value: &str) -> Result<(), ApiKeyError> { + let uuid = uuid::Uuid::parse_str(value).map_err(|_| ApiKeyError::InvalidApiKey)?; + if value.len() != 36 + || uuid.get_version_num() != 4 + || uuid.get_variant() != uuid::Variant::RFC4122 + { + return Err(ApiKeyError::InvalidApiKey); + } + Ok(()) } #[derive(Debug, thiserror::Error)] @@ -46,12 +77,30 @@ pub enum ApiKeyError { #[error("Duplicate key name: {0}")] DuplicateKeyName(String), + #[error("API key value is already registered")] + DuplicateApiKey, + + #[error("API key update must include keyName or apiKey")] + EmptyApiKeyUpdate, + + #[error("keyName cannot be empty")] + InvalidApiKeyName, + + #[error("apiKey must be a UUID v4 in 8-4-4-4-12 format")] + InvalidApiKey, + + #[error("apiKeyType cannot be combined with roles")] + ApiKeyTypeWithRoles, + #[error("Unauthorized: {0}")] Unauthorized(String), #[error("API key revocation could not be synced to all live nodes: {0}")] RevocationSyncFailed(String), + #[error("API key update could not be synced to all live nodes: {0}")] + UpdateSyncFailed(String), + #[error("{0}")] Storage(#[from] crate::storage::ObjectStorageError), @@ -66,9 +115,15 @@ impl actix_web::ResponseError for ApiKeyError { fn status_code(&self) -> actix_web::http::StatusCode { match self { ApiKeyError::KeyNotFound(_) => actix_web::http::StatusCode::NOT_FOUND, - ApiKeyError::DuplicateKeyName(_) => actix_web::http::StatusCode::CONFLICT, + ApiKeyError::DuplicateKeyName(_) | ApiKeyError::DuplicateApiKey => { + actix_web::http::StatusCode::CONFLICT + } + ApiKeyError::InvalidApiKey + | ApiKeyError::ApiKeyTypeWithRoles + | ApiKeyError::EmptyApiKeyUpdate + | ApiKeyError::InvalidApiKeyName => actix_web::http::StatusCode::BAD_REQUEST, ApiKeyError::Unauthorized(_) => actix_web::http::StatusCode::FORBIDDEN, - ApiKeyError::RevocationSyncFailed(_) => { + ApiKeyError::RevocationSyncFailed(_) | ApiKeyError::UpdateSyncFailed(_) => { actix_web::http::StatusCode::SERVICE_UNAVAILABLE } ApiKeyError::Rbac(err) => actix_web::ResponseError::status_code(err), diff --git a/src/handlers/http/apikeys.rs b/src/handlers/http/apikeys.rs index 9b226db7f..6aeff52d1 100644 --- a/src/handlers/http/apikeys.rs +++ b/src/handlers/http/apikeys.rs @@ -6,16 +6,20 @@ use serde::{Deserialize, Deserializer, Serialize, de}; use ulid::Ulid; use crate::{ - apikeys::{ApiKeyError, CreateApiKeyRequest}, + apikeys::{ + ApiKeyError, ApiKeyType, CreateApiKeyRequest, PatchApiKeyRequest, + validate_api_key as validate_api_key_value, + }, handlers::http::{ modal::utils::rbac_utils::{get_metadata, put_metadata}, rbac::{RBACError, UPDATE_LOCK}, }, parseable::DEFAULT_TENANT, rbac::{ - Users, - map::{roles, users}, + GLOBAL_INGESTOR_ROLE, GLOBAL_QUERIER_ROLE, Users, + map::users, role::model::{DefaultPrivilege, Role}, + role_exists, role_for, user::{User, UserType}, }, utils::{get_user_and_tenant_from_request, is_admin}, @@ -183,15 +187,9 @@ fn validate_roles( tenant_id: &Option, ) -> Result<(), ApiKeyError> { let tenant = tenant_or_default(tenant_id); - let tenant_roles = roles(); let missing: Vec = role_names .iter() - .filter(|r| { - tenant_roles - .get(tenant) - .map(|m| !m.contains_key(*r)) - .unwrap_or(true) - }) + .filter(|r| !role_exists(tenant, r)) .cloned() .collect(); if missing.is_empty() { @@ -212,9 +210,25 @@ pub async fn create_api_key( let guard = UPDATE_LOCK.lock().await; - // Reject any role names that don't exist in this tenant while holding the - // update lock so a concurrent role update cannot invalidate the check. - validate_roles(&body.roles, &tenant_id)?; + // Typed keys get server-managed roles; the existing UI/API flow keeps + // assigning explicit roles when apiKeyType is omitted. + let assigned_roles = match body.api_key_type { + Some(api_key_type) => { + if !body.roles.is_empty() { + return Err(ApiKeyError::ApiKeyTypeWithRoles); + } + let role = match api_key_type { + ApiKeyType::Ingestor => GLOBAL_INGESTOR_ROLE, + ApiKeyType::Query => GLOBAL_QUERIER_ROLE, + }; + HashSet::from([role.to_string()]) + } + None => { + // Hold the update lock while checking user-defined roles. + validate_roles(&body.roles, &tenant_id)?; + body.roles + } + }; // Duplicate key-name detection inside the same tenant. let existing = collect_tenant_api_keys(&tenant_id); @@ -227,12 +241,24 @@ pub async fn create_api_key( } let key_id = Ulid::new(); - let api_key_value = generate_api_key_value(); + let api_key_value = if let Some(api_key) = body.api_key { + validate_api_key_value(&api_key)?; + api_key + } else { + generate_api_key_value() + }; + if existing + .iter() + .filter_map(|user: &User| user.as_api_key()) + .any(|key| key.api_key == api_key_value) + { + return Err(ApiKeyError::DuplicateApiKey); + } let user = User::new_api_key( key_id, api_key_value, body.key_name, - body.roles, + assigned_roles, created_by, tenant_id.clone(), ); @@ -268,6 +294,90 @@ pub async fn create_api_key( Ok(HttpResponse::Ok().json(response)) } +/// PATCH /api/prism/v1/apikeys/{key_id} +/// +/// Update a key's name or value without changing its identity or roles. +pub async fn patch_api_key( + req: HttpRequest, + path: web::Path, + web::Json(body): web::Json, +) -> Result { + let (caller_userid, tenant_id) = verify_admin(&req)?; + let key_id_str = path.into_inner(); + let key_id = + Ulid::from_string(&key_id_str).map_err(|_| ApiKeyError::KeyNotFound(key_id_str.clone()))?; + if body.key_name.is_none() && body.api_key.is_none() { + return Err(ApiKeyError::EmptyApiKeyUpdate); + } + + let guard = UPDATE_LOCK.lock().await; + let existing = collect_tenant_api_keys(&tenant_id); + let mut user = existing + .iter() + .find(|user| user.as_api_key().is_some_and(|key| key.key_id == key_id)) + .cloned() + .ok_or_else(|| ApiKeyError::KeyNotFound(key_id_str))?; + let current = user.as_api_key().expect("selected API key user"); + let key_name = body.key_name.unwrap_or_else(|| current.key_name.clone()); + let api_key_value = body.api_key.unwrap_or_else(|| current.api_key.clone()); + + if key_name.trim().is_empty() { + return Err(ApiKeyError::InvalidApiKeyName); + } + if api_key_value != current.api_key { + validate_api_key_value(&api_key_value)?; + } + if existing.iter().any(|other| { + other + .as_api_key() + .is_some_and(|key| key.key_id != key_id && key.key_name == key_name) + }) { + return Err(ApiKeyError::DuplicateKeyName(key_name.clone())); + } + if existing.iter().any(|other| { + other + .as_api_key() + .is_some_and(|key| key.key_id != key_id && key.api_key == api_key_value) + }) { + return Err(ApiKeyError::DuplicateApiKey); + } + + let changed = key_name != current.key_name || api_key_value != current.api_key; + if changed { + let key = match &mut user.ty { + UserType::ApiKey(key) => key, + _ => unreachable!("selected API key user"), + }; + key.key_name.clone_from(&key_name); + key.api_key.clone_from(&api_key_value); + key.modified_at = Utc::now(); + + let mut metadata = get_metadata(&tenant_id).await?; + let stored = metadata + .users + .iter_mut() + .find(|stored| stored.userid() == user.userid()) + .ok_or_else(|| ApiKeyError::KeyNotFound(key_id.to_string()))?; + stored.clone_from(&user); + put_metadata(&metadata, &tenant_id).await?; + Users.put_user(user.clone()); + } + // A retry also re-sends an already applied update to any lagging nodes. + crate::handlers::http::cluster::sync_user_creation( + &req, + user.clone(), + &None, + &tenant_id, + &caller_userid, + ) + .await + .map_err(|error: RBACError| ApiKeyError::UpdateSyncFailed(error.to_string()))?; + drop(guard); + + let response = ApiKeyResponse::try_from(&user)?; + Ok(HttpResponse::Ok().json(response)) +} + /// DELETE /api/prism/v1/apikeys/{key_id} /// /// Delete an API key by key_id. Only admins can delete keys. @@ -334,20 +444,16 @@ pub async fn list_api_keys( ) -> Result { let (_, tenant_id) = verify_admin(&req)?; let tenant = tenant_or_default(&tenant_id); - let tenant_roles = roles(); let entries: Vec = collect_tenant_api_keys(&tenant_id) .iter() .filter(|user| { query.priviledge.as_ref().is_none_or(|privileges| { user.roles.iter().any(|role_name| { - tenant_roles - .get(tenant) - .and_then(|roles| roles.get(role_name)) - .is_some_and(|role| { - privileges - .iter() - .any(|privilege| role_has_privilege(role, privilege)) - }) + role_for(tenant, role_name).is_some_and(|role| { + privileges + .iter() + .any(|privilege| role_has_privilege(&role, privilege)) + }) }) }) }) diff --git a/src/handlers/http/cluster/mod.rs b/src/handlers/http/cluster/mod.rs index 8ecc5538f..f6d2b90e6 100644 --- a/src/handlers/http/cluster/mod.rs +++ b/src/handlers/http/cluster/mod.rs @@ -507,11 +507,17 @@ pub async fn sync_users_with_roles_with_ingestors( })?; if !res.status().is_success() { + let status = res.status(); error!( "failed to forward request to ingestor: {}\nResponse Returned: {:?}", ingestor.domain_name, res.text().await ); + return Err(RBACError::Anyhow(anyhow::anyhow!( + "role sync to {} failed with HTTP {}", + ingestor.domain_name, + status + ))); } Ok(()) @@ -618,11 +624,17 @@ pub async fn sync_user_creation( })?; if !res.status().is_success() { + let status = res.status(); error!( "failed to forward request to node: {}\nResponse Returned: {:?}", node.domain_name, res.text().await ); + return Err(RBACError::Anyhow(anyhow::anyhow!( + "user sync to {} failed with HTTP {}", + node.domain_name, + status + ))); } Ok(()) diff --git a/src/handlers/http/modal/ingest/ingestor_rbac.rs b/src/handlers/http/modal/ingest/ingestor_rbac.rs index 7f1bc4f73..8dbce490f 100644 --- a/src/handlers/http/modal/ingest/ingestor_rbac.rs +++ b/src/handlers/http/modal/ingest/ingestor_rbac.rs @@ -27,8 +27,7 @@ use crate::{ }, parseable::DEFAULT_TENANT, rbac::{ - Users, - map::roles, + Users, role_exists, user::{self, User as ParseableUser}, }, storage, @@ -107,10 +106,7 @@ pub async fn add_roles_to_user( let mut non_existent_roles = Vec::new(); roles_to_add.iter().for_each(|r| { let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); - let role_exists = roles() - .get(tenant) - .is_some_and(|tenant_roles| tenant_roles.contains_key(r)); - if !role_exists { + if !role_exists(tenant, r) { non_existent_roles.push(r.clone()); } }); @@ -153,11 +149,9 @@ pub async fn remove_roles_from_user( // check if all roles exist let mut non_existent_roles = Vec::new(); - roles_to_remove.iter().for_each(|r| { - if let Some(tenant_roles) = roles().get(tenant_id.as_deref().unwrap_or(DEFAULT_TENANT)) - && tenant_roles.get(r).is_none() - { - non_existent_roles.push(r.clone()); + roles_to_remove.iter().for_each(|role| { + if !role_exists(tenant_id.as_deref().unwrap_or(DEFAULT_TENANT), role) { + non_existent_roles.push(role.clone()); } }); @@ -165,17 +159,6 @@ pub async fn remove_roles_from_user( return Err(RBACError::RolesDoNotExist(non_existent_roles)); } - // check that user actually has these roles - let user_roles: HashSet = HashSet::from_iter(Users.get_role(&userid, &tenant_id)); - let roles_not_with_user: HashSet = - HashSet::from_iter(roles_to_remove.difference(&user_roles).cloned()); - - if !roles_not_with_user.is_empty() { - return Err(RBACError::RolesNotAssigned(Vec::from_iter( - roles_not_with_user, - ))); - } - // update parseable.json in staging first let mut metadata = get_metadata(&tenant_id).await?; if let Some(user) = metadata diff --git a/src/handlers/http/modal/ingest/ingestor_role.rs b/src/handlers/http/modal/ingest/ingestor_role.rs index 43c947250..70ff3467d 100644 --- a/src/handlers/http/modal/ingest/ingestor_role.rs +++ b/src/handlers/http/modal/ingest/ingestor_role.rs @@ -29,7 +29,10 @@ use crate::{ role::RoleError, }, parseable::DEFAULT_TENANT, - rbac::map::{mut_roles, mut_sessions, read_user_groups, users}, + rbac::{ + is_system_role, + map::{mut_roles, mut_sessions, read_user_groups, users}, + }, storage, utils::get_tenant_id_from_request, }; @@ -42,6 +45,9 @@ pub async fn put( Json(sync_req): Json, ) -> Result { let name = name.into_inner(); + if is_system_role(&name) { + return Err(RoleError::SystemRolePrivilegesLocked(name)); + } let req_tenant_id = get_tenant_id_from_request(&req); let req_tenant = req_tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); if req_tenant.ne(DEFAULT_TENANT) && (req_tenant_id.ne(&sync_req.tenant_id)) { @@ -100,6 +106,9 @@ pub async fn delete( name: web::Path, ) -> Result { let name = name.into_inner(); + if is_system_role(&name) { + return Err(RoleError::SystemRoleDeletion(name)); + } let req_tenant_id = get_tenant_id_from_request(&req); let mut metadata = get_metadata(&req_tenant_id).await?; diff --git a/src/handlers/http/modal/query/querier_rbac.rs b/src/handlers/http/modal/query/querier_rbac.rs index 576a500b8..6de8953d7 100644 --- a/src/handlers/http/modal/query/querier_rbac.rs +++ b/src/handlers/http/modal/query/querier_rbac.rs @@ -31,8 +31,8 @@ use crate::{ }, parseable::{DEFAULT_TENANT, PARSEABLE}, rbac::{ - Users, - map::{roles, users, write_user_groups}, + self, Users, + map::{users, write_user_groups}, user::{self, UserType}, }, tenants::TENANT_METADATA, @@ -62,11 +62,7 @@ pub async fn post_user( let mut non_existent_roles = Vec::new(); for role in &user_roles { let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); - let role_exists = roles() - .get(tenant) - .map(|tenant_roles| tenant_roles.contains_key(role)) - .unwrap_or(false); - if !role_exists { + if !rbac::role_exists(tenant, role) { non_existent_roles.push(role.clone()); } } @@ -185,6 +181,7 @@ pub async fn add_roles_to_user( let roles_to_add = roles_to_add.into_inner(); let tenant_id = get_tenant_id_from_request(&req); let caller_userid = get_user_from_request(&req)?; + let _guard = UPDATE_LOCK.lock().await; if !Users.contains(&userid, &tenant_id) { return Err(RBACError::UserDoesNotExist); }; @@ -203,9 +200,7 @@ pub async fn add_roles_to_user( // check if the role exists roles_to_add.iter().for_each(|r| { - if let Some(tenant_roles) = roles().get(tenant_id.as_deref().unwrap_or(DEFAULT_TENANT)) - && tenant_roles.get(r).is_none() - { + if !rbac::role_exists(tenant_id.as_deref().unwrap_or(DEFAULT_TENANT), r) { non_existent_roles.push(r.clone()); } }); @@ -229,8 +224,11 @@ pub async fn add_roles_to_user( put_metadata(&metadata, &tenant_id).await?; - // sync to other nodes before updating in-memory (which invalidates sessions) - if let Err(e) = sync_users_with_roles_with_ingestors( + // Keep local state aligned with the durable metadata even if cluster + // fan-out fails. The caller can safely retry the idempotent sync. + Users.add_roles(&userid, roles_to_add.clone(), &tenant_id); + + sync_users_with_roles_with_ingestors( &req, &userid, &roles_to_add, @@ -238,13 +236,7 @@ pub async fn add_roles_to_user( &tenant_id, &caller_userid, ) - .await - { - tracing::error!("Failed to sync role addition to cluster nodes: {e}"); - } - - // update in mem table (this invalidates the user's session) - Users.add_roles(&userid.clone(), roles_to_add.clone(), &tenant_id); + .await?; Ok(format!("Roles updated successfully for {username}")) } @@ -279,9 +271,7 @@ pub async fn remove_roles_from_user( // check if the role exists roles_to_remove.iter().for_each(|r| { - if let Some(tenant_roles) = roles().get(tenant_id.as_deref().unwrap_or(DEFAULT_TENANT)) - && tenant_roles.get(r).is_none() - { + if !rbac::role_exists(tenant_id.as_deref().unwrap_or(DEFAULT_TENANT), r) { non_existent_roles.push(r.clone()); } }); @@ -290,16 +280,6 @@ pub async fn remove_roles_from_user( return Err(RBACError::RolesDoNotExist(non_existent_roles)); } - // check for role not present with user - let user_roles: HashSet = HashSet::from_iter(Users.get_role(&userid, &tenant_id)); - let roles_not_with_user: HashSet = - HashSet::from_iter(roles_to_remove.difference(&user_roles).cloned()); - if !roles_not_with_user.is_empty() { - return Err(RBACError::RolesNotAssigned(Vec::from_iter( - roles_not_with_user, - ))); - } - // In multi-tenant, prevent removing the admin role from the tenant owner if PARSEABLE.options.is_multi_tenant() && roles_to_remove.contains("admin") @@ -332,10 +312,9 @@ pub async fn remove_roles_from_user( return Err(RBACError::UserDoesNotExist); } - put_metadata(&metadata, &tenant_id).await?; - - // sync to other nodes before updating in-memory (which invalidates sessions) - if let Err(e) = sync_users_with_roles_with_ingestors( + // Sync before removing the role locally, so the UI can retry if sync fails. + // Ingestors accept the retry even if they already removed the role. + sync_users_with_roles_with_ingestors( &req, &userid, &roles_to_remove, @@ -343,13 +322,10 @@ pub async fn remove_roles_from_user( &tenant_id, &caller_userid, ) - .await - { - tracing::error!("Failed to sync role removal to cluster nodes: {e}"); - } + .await?; - // update in mem table (this invalidates the user's session) - Users.remove_roles(&userid.clone(), roles_to_remove.clone(), &tenant_id); + put_metadata(&metadata, &tenant_id).await?; + Users.remove_roles(&userid, roles_to_remove.clone(), &tenant_id); Ok(HttpResponse::Ok().json(format!("Roles updated successfully for {username}"))) } diff --git a/src/handlers/http/modal/query/querier_role.rs b/src/handlers/http/modal/query/querier_role.rs index 321efa9b0..b9b9afa67 100644 --- a/src/handlers/http/modal/query/querier_role.rs +++ b/src/handlers/http/modal/query/querier_role.rs @@ -31,6 +31,7 @@ use crate::{ }, parseable::DEFAULT_TENANT, rbac::{ + is_system_role, map::{mut_roles, mut_sessions, read_user_groups, roles, users}, role::model::{Role, RoleType}, roles_to_permission, @@ -47,6 +48,9 @@ pub async fn put( Json(body): Json, ) -> Result { let name = name.into_inner(); + if is_system_role(&name) { + return Err(RoleError::SystemRolePrivilegesLocked(name)); + } let tenant_id = get_tenant_id_from_request(&req); let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); // extract caller userid early, before any session mutations @@ -130,6 +134,9 @@ pub async fn delete( name: web::Path, ) -> Result { let name = name.into_inner(); + if is_system_role(&name) { + return Err(RoleError::SystemRoleDeletion(name)); + } let tenant_id = get_tenant_id_from_request(&req); let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); // extract caller userid early, before any session mutations diff --git a/src/handlers/http/modal/server.rs b/src/handlers/http/modal/server.rs index 7b58e91da..6d1fee80a 100644 --- a/src/handlers/http/modal/server.rs +++ b/src/handlers/http/modal/server.rs @@ -270,6 +270,11 @@ impl Server { .service( resource("/{key_id}") .route(web::get().to(apikeys::get_api_key).authorize(Action::All)) + .route( + web::patch() + .to(apikeys::patch_api_key) + .authorize(Action::All), + ) .route( web::delete() .to(apikeys::delete_api_key) diff --git a/src/handlers/http/rbac.rs b/src/handlers/http/rbac.rs index 6d5bf1896..bc746ff36 100644 --- a/src/handlers/http/rbac.rs +++ b/src/handlers/http/rbac.rs @@ -150,9 +150,7 @@ pub async fn post_user( let mut non_existent_roles = Vec::new(); for role in &user_roles { - if let Some(tenant_roles) = roles().get(tenant_id.as_deref().unwrap_or(DEFAULT_TENANT)) - && !tenant_roles.contains_key(role) - { + if !rbac::role_exists(tenant_id.as_deref().unwrap_or(DEFAULT_TENANT), role) { non_existent_roles.push(role.clone()); } } @@ -294,13 +292,7 @@ pub fn get_role_internal( .get_role(userid, tenant_id) .iter() .filter_map(|role_name| { - if let Some(roles) = roles().get(tenant) - && let Some(role) = roles.get(role_name) - { - Some((role_name.to_owned(), role.clone())) - } else { - None - } + rbac::role_for(tenant, role_name).map(|role| (role_name.clone(), role)) }) .collect(); @@ -424,9 +416,7 @@ pub async fn add_roles_to_user( // check if the role exists for role in &roles_to_add { - if let Some(tenant_roles) = roles().get(tenant_str) - && !tenant_roles.contains_key(role) - { + if !rbac::role_exists(tenant_str, role) { non_existent_roles.push(role.clone()); } } @@ -488,9 +478,7 @@ pub async fn remove_roles_from_user( // check if the role exists for role in &roles_to_remove { - if let Some(tenant_roles) = roles().get(tenant_id.as_deref().unwrap_or(DEFAULT_TENANT)) - && !tenant_roles.contains_key(role) - { + if !rbac::role_exists(tenant_id.as_deref().unwrap_or(DEFAULT_TENANT), role) { non_existent_roles.push(role.clone()); } } diff --git a/src/handlers/http/role.rs b/src/handlers/http/role.rs index 969020c24..ba6d90779 100644 --- a/src/handlers/http/role.rs +++ b/src/handlers/http/role.rs @@ -27,6 +27,7 @@ use actix_web::{ use crate::rbac::map::roles; use crate::rbac::role::model::{Role, RoleType, RoleUI}; +use crate::rbac::{GLOBAL_INGESTOR_ROLE, GLOBAL_QUERIER_ROLE, is_system_role, system_role}; use crate::{ parseable::{DEFAULT_TENANT, PARSEABLE}, rbac::map::{DEFAULT_ROLE, mut_roles, mut_sessions, read_user_groups, users}, @@ -42,14 +43,17 @@ pub async fn put( name: web::Path, Json(role): Json, ) -> Result { + let name = name.into_inner(); // internal role manipulation not allowed + if is_system_role(&name) { + return Err(RoleError::SystemRolePrivilegesLocked(name)); + } if role.role_type().eq(&RoleType::Internal) { return Err(RoleError::ProtectedRole); } if role.deny_super_admin() { return Err(RoleError::SuperAdminPrivilege); } - let name = name.into_inner(); let tenant_id = get_tenant_id_from_request(&req); // validate the role name @@ -104,6 +108,9 @@ pub async fn get(req: HttpRequest, name: web::Path) -> Result) -> Result { + if let Some(role) = system_role(name) { + return Ok(role); + } let metadata = get_metadata(tenant_id).await?; let role = metadata.roles.get(name).cloned().unwrap_or_default(); if role.role_type().eq(&RoleType::Internal) { @@ -130,6 +137,11 @@ pub async fn list_internal( roles.insert(k, RoleUI(r)); } } + for name in [GLOBAL_INGESTOR_ROLE, GLOBAL_QUERIER_ROLE] { + if let Some(role) = system_role(name) { + roles.insert(name.to_string(), RoleUI(role)); + } + } Ok(roles) } @@ -141,6 +153,9 @@ pub async fn delete( name: web::Path, ) -> Result { let name = name.into_inner(); + if is_system_role(&name) { + return Err(RoleError::SystemRoleDeletion(name)); + } let tenant_id = get_tenant_id_from_request(&req); let tenant = tenant_id.as_deref().unwrap_or(DEFAULT_TENANT); if let Some(tenant_roles) = roles().get(tenant) @@ -257,6 +272,10 @@ pub enum RoleError { RoleInUse, #[error("Cannot perform this operation as role is assigned to a protected user.")] ProtectedRole, + #[error("System role '{0}' cannot be deleted.")] + SystemRoleDeletion(String), + #[error("Privileges of system role '{0}' are fixed; you cannot add or remove them.")] + SystemRolePrivilegesLocked(String), #[error("Error: {0}")] Anyhow(#[from] anyhow::Error), #[error("{0}")] @@ -276,6 +295,9 @@ impl actix_web::ResponseError for RoleError { Self::RoleInUse => StatusCode::BAD_REQUEST, Self::SuperAdminPrivilege => StatusCode::BAD_REQUEST, Self::ProtectedRole => StatusCode::BAD_REQUEST, + Self::SystemRoleDeletion(_) | Self::SystemRolePrivilegesLocked(_) => { + StatusCode::BAD_REQUEST + } Self::Anyhow(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::SerdeError(_) => StatusCode::BAD_REQUEST, Self::Network(_) => StatusCode::BAD_GATEWAY, diff --git a/src/rbac/mod.rs b/src/rbac/mod.rs index 0e7dda57d..66c97f61d 100644 --- a/src/rbac/mod.rs +++ b/src/rbac/mod.rs @@ -36,15 +36,52 @@ use crate::handlers::http::rbac::RBACError; use crate::parseable::DEFAULT_TENANT; use crate::rbac::map::{mut_sessions, mut_users, read_user_groups, roles, sessions, users}; use crate::rbac::role::Action; -use crate::rbac::role::model::RoleUI; +use crate::rbac::role::model::{Role, RoleUI}; use crate::rbac::user::User; use crate::utils::get_tenant_id_from_key; use self::map::SessionKey; -use self::role::{Permission, RoleBuilder}; +use self::role::{ParseableResourceType, Permission, RoleBuilder, model::DefaultPrivilege}; use self::user::UserType; pub const EXPIRY_DURATION: Duration = Duration::hours(1); +/// Reserved, server-managed role for global ingestion. +/// This name is outside the user-defined role-name grammar. +pub const GLOBAL_INGESTOR_ROLE: &str = "__global_ingestor__"; +/// Reserved, server-managed role with the existing reader privilege on all datasets. +pub const GLOBAL_QUERIER_ROLE: &str = "__global_querier__"; + +pub fn system_role(name: &str) -> Option { + let privilege = match name { + GLOBAL_INGESTOR_ROLE => DefaultPrivilege::Ingestor { + resource: Some(ParseableResourceType::Stream("*".to_string())), + }, + GLOBAL_QUERIER_ROLE => DefaultPrivilege::Reader { + resource: Some(ParseableResourceType::Stream("*".to_string())), + }, + _ => return None, + }; + Some(Role::create_internal_role(vec![privilege])) +} + +pub fn is_system_role(name: &str) -> bool { + matches!(name, GLOBAL_INGESTOR_ROLE | GLOBAL_QUERIER_ROLE) +} + +pub fn role_for(tenant_id: &str, name: &str) -> Option { + system_role(name).or_else(|| { + roles() + .get(tenant_id) + .and_then(|roles| roles.get(name).cloned()) + }) +} + +pub fn role_exists(tenant_id: &str, name: &str) -> bool { + is_system_role(name) + || roles() + .get(tenant_id) + .is_some_and(|roles| roles.contains_key(name)) +} #[derive(PartialEq, Debug)] pub enum Response { @@ -397,6 +434,12 @@ pub struct UsersPrism { pub fn roles_to_permission(roles: Vec, tenant_id: &str) -> Vec { let mut perms = HashSet::new(); for role in &roles { + if let Some(system_role) = system_role(role) { + for privilege in system_role.privileges() { + perms.extend(RoleBuilder::from(privilege).build()); + } + continue; + } let role_map = &map::roles(); if let Some(roles) = role_map.get(tenant_id) && let Some(privilege_list) = roles.get(role) diff --git a/src/rbac/utils.rs b/src/rbac/utils.rs index 435fc4b1e..c2eda9fb8 100644 --- a/src/rbac/utils.rs +++ b/src/rbac/utils.rs @@ -21,7 +21,7 @@ use url::Url; use crate::{ parseable::{DEFAULT_TENANT, PARSEABLE}, - rbac::{map::read_user_groups, role::model::RoleUI}, + rbac::{map::read_user_groups, role::model::RoleUI, role_for}, }; use super::{ @@ -63,19 +63,7 @@ pub fn to_prism_user(user: &User) -> UsersPrism { .get_role(id, &user.tenant) .iter() .filter_map(|role_name| { - roles() - .get(tenant_id) - .filter(|roles| roles.get(role_name).is_some()) - // .map(|roles| { - // if let Some(role) = roles.get(role_name) { - // (role_name.to_owned(), role.clone()) - // } - // }) - // .get(role_name) - .map(|roles| { - let role = roles.get(role_name).unwrap(); - (role_name.to_owned(), RoleUI(role.clone())) - }) + role_for(tenant_id, role_name).map(|role| (role_name.clone(), RoleUI(role))) }) .collect();