From 8838c2e2d5f00b7267569282c2a68c5c1d59d1ab Mon Sep 17 00:00:00 2001 From: Tobin Feldman-Fitzthum Date: Mon, 15 Dec 2025 11:40:26 -0800 Subject: [PATCH 1/5] admin: return admin role from admin backend When the admin backend validates a token, it should extract a role string and return it. The role will be used by the common admin code to grant access to specific endpoints. Since the ALlowAll endpoint does not have any roles, it will always return "Anonymous" as the role. Signed-off-by: Tobin Feldman-Fitzthum --- kbs/src/admin/allow_all.rs | 4 ++-- kbs/src/admin/deny_all.rs | 2 +- kbs/src/admin/mod.rs | 13 ++++++++++--- kbs/src/admin/simple.rs | 15 +++++---------- 4 files changed, 18 insertions(+), 16 deletions(-) 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/mod.rs b/kbs/src/admin/mod.rs index d14c61ff15..3a3e846da9 100644 --- a/kbs/src/admin/mod.rs +++ b/kbs/src/admin/mod.rs @@ -67,7 +67,11 @@ 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()), + Ok(ref role) => info!( + "Allowing Admin access to {} for {}", + request.full_url().as_str(), + role + ), Err(ref e) => info!( "Not allowing Admin access for {} due to: \n{}", request.full_url().as_str(), @@ -75,7 +79,8 @@ impl Admin { ), } - res + // Don't pass the role to the caller. + res.map(|_| ()) } } @@ -85,5 +90,7 @@ 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; } 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) } } From 6a1036c2ed63800f4ad030f1ec995f6e2742e4cc Mon Sep 17 00:00:00 2001 From: Tobin Feldman-Fitzthum Date: Mon, 15 Dec 2025 13:46:44 -0800 Subject: [PATCH 2/5] admin: add admin roles Introduce the admin role configuration at the admin level (rather than the admin backend level). This way, we don't need to duplicate the admin logic for each backend. An admin role matches a particular admin id to a regex. The regex controls which endpoints are allowed. In the future, we could make both fiels regular expressions to more flexibly match different admin ids. If no roles are specified, all admin will have access to all endpoints. This provides backwards compatibility. Signed-off-by: Tobin Feldman-Fitzthum --- kbs/src/admin/error.rs | 6 ++++ kbs/src/admin/mod.rs | 74 ++++++++++++++++++++++++++++++++---------- kbs/src/api_server.rs | 12 +++---- kbs/src/config.rs | 9 +++++ 4 files changed, 78 insertions(+), 23 deletions(-) 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 3a3e846da9..d3c1f86324 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,8 +47,27 @@ 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)] +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 { type Error = Error; fn try_from(value: AdminConfig) -> Result { @@ -59,28 +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(ref role) => info!( - "Allowing Admin access to {} for {}", - request.full_url().as_str(), - role - ), - 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(()); + } + } } - // Don't pass the role to the caller. - res.map(|_| ()) + info!( + "Not allowing Admin access to {}", + request.full_url().as_str() + ); + 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(), From 2eea9090e870400349e246f0368c360795a217b3 Mon Sep 17 00:00:00 2001 From: Tobin Feldman-Fitzthum Date: Tue, 16 Dec 2025 09:47:30 -0800 Subject: [PATCH 3/5] admin: add unit tests for admin roles Add unit tests for admin roles. Try creating the Admin struct with a few different configs, and validate some requests. I find putting a bunch of JSON into the rstest macro to be hard to read, so these tests just do everything in Rust. Signed-off-by: Tobin Feldman-Fitzthum --- kbs/src/admin/mod.rs | 107 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/kbs/src/admin/mod.rs b/kbs/src/admin/mod.rs index d3c1f86324..cb7c3ef129 100644 --- a/kbs/src/admin/mod.rs +++ b/kbs/src/admin/mod.rs @@ -57,6 +57,7 @@ pub struct AdminConfig { /// 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. @@ -67,7 +68,6 @@ pub struct AdminRole { pub allowed_endpoints: String, } - impl TryFrom for Admin { type Error = Error; fn try_from(value: AdminConfig) -> Result { @@ -134,3 +134,108 @@ pub(crate) trait AdminBackend: Send + Sync { /// 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()); + } +} From c1a91a77c05c4c6673145c0b7a7cad8c9781e7c5 Mon Sep 17 00:00:00 2001 From: Tobin Feldman-Fitzthum Date: Tue, 16 Dec 2025 12:15:42 -0800 Subject: [PATCH 4/5] tests: fixup integration test for admin roles Quick fixup of the integration test. An integration test using roles will follow in a future PR. Signed-off-by: Tobin Feldman-Fitzthum --- integration-tests/src/common.rs | 2 ++ 1 file changed, 2 insertions(+) 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(), }, }; From 2fb2e3df9329c7388a7ceef27167ab71c2a456f9 Mon Sep 17 00:00:00 2001 From: Tobin Feldman-Fitzthum Date: Mon, 22 Dec 2025 07:52:37 -0800 Subject: [PATCH 5/5] docs: add documentation about admin roles Update the KBS config doc to include the structure of the admin roles configuration and a description of how admin roles work. Signed-off-by: Tobin Feldman-Fitzthum --- kbs/docs/config.md | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) 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