diff --git a/integration-tests/src/common.rs b/integration-tests/src/common.rs index 1526fc11f6..3e280ea515 100644 --- a/integration-tests/src/common.rs +++ b/integration-tests/src/common.rs @@ -193,9 +193,11 @@ impl TestHarness { public_key_path: auth_pubkey_path.as_path().to_path_buf(), }], }), + roles: Vec::new(), }, AdminType::DenyAll => AdminConfig { admin_backend: AdminBackendType::DenyAll, + roles: Vec::new(), }, }; diff --git a/kbs/docs/config.md b/kbs/docs/config.md index 561f60b7bc..127a336244 100644 --- a/kbs/docs/config.md +++ b/kbs/docs/config.md @@ -186,15 +186,23 @@ Detailed [documentation](https://docs.trustauthority.intel.com). ### Admin API Configuration -Multiple Admin backends are available. These control access to admin endpoints such as `set_policy`. -Today, the available backends are `DenyAll` (disables admin endpoints), `InsecureAllowAll` (for debugging) -and `Simple`. +The admin configuration has two parts. +The admin backend is used for validating the admin token and extracting a role. +The admin configuration can also specify which endpoints the role has access to. + + +Multiple Admin backends are available. +Today, the available backends are `DenyAll` (rejects all admin tokens, thereby blocking all admin interfaces), +`InsecureAllowAll` (treats every request as if there is a valid admin token and return the role `Anonymous`) +and `Simple` (checks the admin token based on a set of public keys). + By default, the simple backend will be used, but no personas will be enabled. Use the `type` field to set the admin backend. | Property | Type | Description | Required | Default | |-------------------|---------|-------------------------------------------------------------------|----------|---------| -| `type` | String | The backend used to validate admiin requests. | No | Simple | +| `type` | String | The backend used to validate admin requests. | No | Simple | +| `roles` | | Rules for accessing admin endpoints | No | Empty | If the `Simple` backend is used, a list of admin personas can be provided, each with the following properties: @@ -203,6 +211,23 @@ If the `Simple` backend is used, a list of admin personas can be provided, each | `id` | String | A string used to identify the admin. | Yes | Simple | | `public_key_path` | String | The path to the public key corresponding to the admin token. | Yes | Simple | +The role field can specify rules that govern which endpoints an admin is allowed to visit. +If no roles are specified, all admin can access all admin endpoints. +Otherwise, the roles field should be a list of role entries. +One role entry looks like this. + +| Property | Type | Description | Required | +|-------------------|--------|----------------------------------------------------------|----------| +| id | String | The role that this rule applies to | Yes | +| allowed_endpoints | String | A regular expression matching endpoints that are allowed | Yes | + +After the admin token is validated, the role extracted from the token will be checked against +the roles specified in the config file. +If a matching role is found, the `allowed_endpoints` regular expression is used to determine +which endpoints the admin can access. +If no matching role is found (and at least one role is specified), the admin will not be able +to access any endpoints. +Roles and role ids are case insensitive. ### Policy Engine Configuration diff --git a/kbs/src/admin/allow_all.rs b/kbs/src/admin/allow_all.rs index 68a7881d48..c17360075d 100644 --- a/kbs/src/admin/allow_all.rs +++ b/kbs/src/admin/allow_all.rs @@ -12,8 +12,8 @@ use crate::admin::AdminBackend; pub struct InsecureAllowAllBackend {} impl AdminBackend for InsecureAllowAllBackend { - fn validate_admin_token(&self, _request: &HttpRequest) -> Result<()> { + fn validate_admin_token(&self, _request: &HttpRequest) -> Result { warn!("Allow All admin backend is set. Anyone can access admin APIs"); - Ok(()) + Ok("Anonymous".to_string()) } } diff --git a/kbs/src/admin/deny_all.rs b/kbs/src/admin/deny_all.rs index ba89d53556..d1557f8db1 100644 --- a/kbs/src/admin/deny_all.rs +++ b/kbs/src/admin/deny_all.rs @@ -12,7 +12,7 @@ use crate::admin::AdminBackend; pub struct DenyAllBackend {} impl AdminBackend for DenyAllBackend { - fn validate_admin_token(&self, _request: &HttpRequest) -> Result<()> { + fn validate_admin_token(&self, _request: &HttpRequest) -> Result { warn!("Admin endpoints are disabled"); Err(Error::AdminEndpointsDisabled) } diff --git a/kbs/src/admin/error.rs b/kbs/src/admin/error.rs index 8e2126b4bf..f11d76c3cf 100644 --- a/kbs/src/admin/error.rs +++ b/kbs/src/admin/error.rs @@ -16,6 +16,12 @@ pub enum Error { #[error("Admin endpoints disabled.")] AdminEndpointsDisabled, + #[error("Duplicate Admin Role")] + DuplicateAdminRole, + + #[error("Invalid Regular Expression in Role")] + InvalidRoleRegex(#[from] regex::Error), + #[error("`auth_public_key` is not set in the config file")] NoPublicKeyGiven, diff --git a/kbs/src/admin/mod.rs b/kbs/src/admin/mod.rs index d14c61ff15..cb7c3ef129 100644 --- a/kbs/src/admin/mod.rs +++ b/kbs/src/admin/mod.rs @@ -4,7 +4,9 @@ use actix_web::HttpRequest; use log::{info, warn}; +use regex::Regex; use serde::Deserialize; +use std::collections::HashMap; use std::sync::Arc; pub mod allow_all; @@ -21,9 +23,9 @@ use simple::{SimpleAdminBackend, SimpleAdminConfig}; #[derive(Clone)] pub(crate) struct Admin { backend: Arc, + roles: HashMap, } -// create a simple backend #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(tag = "type")] pub enum AdminBackendType { @@ -45,6 +47,25 @@ impl Default for AdminBackendType { pub struct AdminConfig { #[serde(flatten)] pub admin_backend: AdminBackendType, + /// Admin roles control which admin personas can access + /// which endpoints. + /// + /// If no admin roles are specified, all admin will be able + /// to access all endpoints. + pub roles: Vec, +} + +/// An admin role is a rule that grants access for some roles to some endpoints. +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct AdminRole { + /// The admin role that this rule applies to. + /// The id is case insensitive. + pub id: String, + /// A regular expression selecting request paths this rule allows. + /// In other words, the paths that the above role can access. + #[serde(default)] + pub allowed_endpoints: String, } impl TryFrom for Admin { @@ -59,23 +80,47 @@ impl TryFrom for Admin { AdminBackendType::DenyAll => Arc::new(DenyAllBackend::default()) as _, }; - Ok(Admin { backend }) + // Parse roles to ensure valid regexes and no duplicates. + let mut roles = HashMap::new(); + for role in value.roles { + let re = Regex::new(&role.allowed_endpoints)?; + + if roles.insert(role.id.to_lowercase(), re).is_some() { + return Err(Error::DuplicateAdminRole); + } + } + + Ok(Admin { backend, roles }) } } impl Admin { - pub fn validate_admin_token(&self, request: &HttpRequest) -> Result<()> { - let res = self.backend.validate_admin_token(request); - match res { - Ok(()) => info!("Allowing Admin access for {}", request.full_url().as_str()), - Err(ref e) => info!( - "Not allowing Admin access for {} due to: \n{}", - request.full_url().as_str(), - e - ), + pub fn check_admin_access(&self, request: &HttpRequest) -> Result<()> { + if let Ok(role) = self.backend.validate_admin_token(request) { + info!("Admin Role: {role}"); + + // If there are no roles specified, allow all. + if self.roles.is_empty() { + info!( + "No admin roles configured. Allowing Request to {}", + request.full_url().as_str() + ); + return Ok(()); + } + + if let Some(re) = self.roles.get(&role.to_lowercase()) { + if re.is_match(&request.uri().to_string()) { + info!("Allowing Request to {}", request.full_url().as_str()); + return Ok(()); + } + } } - res + info!( + "Not allowing Admin access to {}", + request.full_url().as_str() + ); + Err(Error::AdminAccessDenied) } } @@ -85,5 +130,112 @@ pub(crate) trait AdminBackend: Send + Sync { /// When a request is made to an admin endpoint, this method should be called /// to validate that the user making the request is authorized /// to access admin functionality. - fn validate_admin_token(&self, request: &HttpRequest) -> Result<()>; + /// + /// If the token is valid, the backend will return an admin role. + fn validate_admin_token(&self, request: &HttpRequest) -> Result; +} + +#[cfg(test)] +mod tests { + + use super::*; + use serde_json::json; + + #[test] + pub fn make_admin_object() { + // basic (backwards compatible) + let admin_config_json = json!({ + "type": "InsecureAllowAll", + }); + + let config: AdminConfig = serde_json::from_value(admin_config_json).unwrap(); + let _admin = Admin::try_from(config).unwrap(); + + // with invalid role (wrong field name) + let admin_config_json = json!({ + "type": "InsecureAllowAll", + "roles" : [{ + "id": "Anonymous", + "allowed_paths": "xyz" + }] + }); + + let config = serde_json::from_value::(admin_config_json); + assert!(config.is_err()); + + // with invalid role (bad regex) + let admin_config_json = json!({ + "type": "InsecureAllowAll", + "roles" : [{ + "id": "Anonymous", + "allowed_endpoints": "(xyz" + }] + }); + + let config: AdminConfig = serde_json::from_value(admin_config_json).unwrap(); + let admin = Admin::try_from(config); + assert!(admin.is_err()); + + // with invalid role (duplicate role) + let admin_config_json = json!({ + "type": "InsecureAllowAll", + "roles" : [{ + "id": "Anonymous", + "allowed_endpoints": "xyz" + }, + { + "id": "Anonymous", + "allowed_endpoints": "abc" + }] + }); + + let config: AdminConfig = serde_json::from_value(admin_config_json).unwrap(); + let admin = Admin::try_from(config); + assert!(admin.is_err()); + } + + #[test] + pub fn check_requests() { + let admin_config_json = json!({ + "type": "InsecureAllowAll", + "roles" : [{ + "id": "Anonymous", + "allowed_endpoints": "^/resource/a/.+/c$" + }] + }); + + let config: AdminConfig = serde_json::from_value(admin_config_json).unwrap(); + let admin = Admin::try_from(config).unwrap(); + + // valid request + let req = actix_web::test::TestRequest::post() + .uri("/resource/a/b/c") + .to_http_request(); + admin.check_admin_access(&req).unwrap(); + + // invalid request + let req = actix_web::test::TestRequest::post() + .uri("/resource/b/b/c") + .to_http_request(); + assert!(admin.check_admin_access(&req).is_err()); + } + + #[test] + pub fn check_requests_wrong_role() { + let admin_config_json = json!({ + "type": "InsecureAllowAll", + "roles" : [{ + "id": "Steve", + "allowed_endpoints": "^/resource/a/.+/c$" + }] + }); + + let config: AdminConfig = serde_json::from_value(admin_config_json).unwrap(); + let admin = Admin::try_from(config).unwrap(); + + let req = actix_web::test::TestRequest::post() + .uri("/resource/a/b/c") + .to_http_request(); + assert!(admin.check_admin_access(&req).is_err()); + } } diff --git a/kbs/src/admin/simple.rs b/kbs/src/admin/simple.rs index 90dfe65a11..811918a67e 100644 --- a/kbs/src/admin/simple.rs +++ b/kbs/src/admin/simple.rs @@ -56,9 +56,7 @@ impl SimpleAdminBackend { } impl AdminBackend for SimpleAdminBackend { - fn validate_admin_token(&self, request: &HttpRequest) -> Result<()> { - let mut token_validated = false; - + fn validate_admin_token(&self, request: &HttpRequest) -> Result { let bearer = Authorization::::parse(request)?.into_scheme(); let token = bearer.token(); @@ -68,9 +66,10 @@ impl AdminBackend for SimpleAdminBackend { .verify_token::(token, Some(VerificationOptions::default())); match res { Ok(_claims) => { - token_validated = true; info!("Admin access granted for {}", persona.id); - break; + + // Return the first matching persona + return Ok(persona.id.clone()); } Err(e) => { info!("Access not granted for {} due to: \n{}", persona.id, e); @@ -78,10 +77,6 @@ impl AdminBackend for SimpleAdminBackend { } } - if !token_validated { - Err(Error::AdminAccessDenied) - } else { - Ok(()) - } + Err(Error::AdminAccessDenied) } } diff --git a/kbs/src/api_server.rs b/kbs/src/api_server.rs index 6a54021a2a..b8e28b0d2a 100644 --- a/kbs/src/api_server.rs +++ b/kbs/src/api_server.rs @@ -217,7 +217,7 @@ pub(crate) async fn api( .map_err(From::from), #[cfg(feature = "as")] "attestation-policy" if request.method() == Method::POST => { - core.admin.validate_admin_token(&request)?; + core.admin.check_admin_access(&request)?; core.attestation_service.set_policy(&body).await?; Ok(HttpResponse::Ok().finish()) @@ -226,7 +226,7 @@ pub(crate) async fn api( // Reference value querying API is exposed as // GET /reference-value/ "reference-value" if request.method() == Method::GET => { - core.admin.validate_admin_token(&request)?; + core.admin.check_admin_access(&request)?; let reference_value_id = resource_path.join("/"); let reference_values = core .attestation_service @@ -242,7 +242,7 @@ pub(crate) async fn api( } #[cfg(feature = "as")] "reference-value" if request.method() == Method::POST => { - core.admin.validate_admin_token(&request)?; + core.admin.check_admin_access(&request)?; let message = std::str::from_utf8(&body).map_err(|_| Error::RvpsError { message: "Failed to parse reference value message".to_string(), })?; @@ -262,7 +262,7 @@ pub(crate) async fn api( // TODO: consider to rename the api name for it is not only for // resource retrievement but for all plugins. "resource-policy" if request.method() == Method::POST => { - core.admin.validate_admin_token(&request)?; + core.admin.check_admin_access(&request)?; core.policy_engine.set_policy(&body).await?; Ok(HttpResponse::Ok().finish()) @@ -270,7 +270,7 @@ pub(crate) async fn api( // TODO: consider to rename the api name for it is not only for // resource retrievement but for all plugins. "resource-policy" if request.method() == Method::GET => { - core.admin.validate_admin_token(&request)?; + core.admin.check_admin_access(&request)?; let policy = core.policy_engine.get_policy().await?; Ok(HttpResponse::Ok().content_type("text/xml").body(policy)) @@ -292,7 +292,7 @@ pub(crate) async fn api( .map_err(|e| Error::PluginInternalError { source: e })? { // Plugin calls need to be authorized by the admin auth - core.admin.validate_admin_token(&request)?; + core.admin.check_admin_access(&request)?; let response = plugin .handle(&body, &query, resource_path, request.method()) .await diff --git a/kbs/src/config.rs b/kbs/src/config.rs index 3aa39ef47f..373da1c2f7 100644 --- a/kbs/src/config.rs +++ b/kbs/src/config.rs @@ -169,6 +169,7 @@ mod tests { }, admin: AdminConfig { admin_backend: AdminBackendType::DenyAll, + roles: Vec::new(), }, policy_engine: PolicyEngineConfig { policy_path: PathBuf::from("/etc/kbs-policy.rego"), @@ -219,6 +220,7 @@ mod tests { }, admin: AdminConfig { admin_backend: AdminBackendType::DenyAll, + roles: Vec::new(), }, policy_engine: PolicyEngineConfig { policy_path: DEFAULT_POLICY_PATH.into(), @@ -256,6 +258,7 @@ mod tests { }, admin: AdminConfig { admin_backend: AdminBackendType::DenyAll, + roles: Vec::new(), }, policy_engine: PolicyEngineConfig { policy_path: PathBuf::from("/etc/kbs-policy.rego"), @@ -299,6 +302,7 @@ mod tests { public_key_path: "/opt/confidential-containers/trustee/admin1-pubkey.pem".into() }], }), + roles: Vec::new(), }, policy_engine: PolicyEngineConfig::default(), plugins: Vec::new(), @@ -341,6 +345,7 @@ mod tests { }, admin: AdminConfig { admin_backend: AdminBackendType::InsecureAllowAll, + roles: Vec::new(), }, policy_engine: PolicyEngineConfig::default(), plugins: Vec::new(), @@ -376,6 +381,7 @@ mod tests { }, admin: AdminConfig { admin_backend: AdminBackendType::DenyAll, + roles: Vec::new(), }, policy_engine: PolicyEngineConfig::default(), plugins: Vec::new(), @@ -401,6 +407,7 @@ mod tests { }, admin: AdminConfig { admin_backend: AdminBackendType::DenyAll, + roles: Vec::new(), }, policy_engine: PolicyEngineConfig::default(), plugins: Vec::new(), @@ -432,6 +439,7 @@ mod tests { }, admin: AdminConfig { admin_backend: AdminBackendType::DenyAll, + roles: Vec::new(), }, policy_engine: PolicyEngineConfig::default(), plugins: Vec::new(), @@ -468,6 +476,7 @@ mod tests { admin_backend: AdminBackendType::Simple(SimpleAdminConfig { personas: Vec::new(), }), + roles: Vec::new(), }, policy_engine: PolicyEngineConfig { policy_path: "/opa/confidential-containers/kbs/policy.rego".into(),