Enforce local KV write capabilities
This commit is contained in:
parent
1ab24631cd
commit
5751748458
8 changed files with 118 additions and 10 deletions
|
|
@ -285,6 +285,8 @@ pub enum KvCommand {
|
|||
name: String,
|
||||
key: String,
|
||||
value: String,
|
||||
#[arg(long)]
|
||||
subject: Option<String>,
|
||||
},
|
||||
Get {
|
||||
name: String,
|
||||
|
|
@ -564,7 +566,17 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
},
|
||||
Command::Kv { command } => match command {
|
||||
KvCommand::Create { name } => ControlRequest::KvCreate { name },
|
||||
KvCommand::Set { name, key, value } => ControlRequest::KvSet { name, key, value },
|
||||
KvCommand::Set {
|
||||
name,
|
||||
key,
|
||||
value,
|
||||
subject,
|
||||
} => ControlRequest::KvSet {
|
||||
name,
|
||||
key,
|
||||
value,
|
||||
subject,
|
||||
},
|
||||
KvCommand::Get { name, key } => ControlRequest::KvGet { name, key },
|
||||
},
|
||||
Command::Pubsub { command } => match command {
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ pub enum ControlRequest {
|
|||
name: String,
|
||||
key: String,
|
||||
value: String,
|
||||
subject: Option<String>,
|
||||
},
|
||||
KvGet {
|
||||
name: String,
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ pub enum NodeError {
|
|||
Document(#[from] geth_document::DocumentError),
|
||||
#[error("resource not found: {0}")]
|
||||
ResourceNotFound(String),
|
||||
#[error("unauthorized: {0}")]
|
||||
Unauthorized(String),
|
||||
#[error("secrets error: {0}")]
|
||||
Secrets(#[from] geth_secrets::SecretsError),
|
||||
#[error("pubsub error: {0}")]
|
||||
|
|
@ -1009,12 +1011,18 @@ pub fn handle_request(
|
|||
kv: kv_resource_from_stored(&stored),
|
||||
})
|
||||
}
|
||||
ControlRequest::KvSet { name, key, value } => {
|
||||
ControlRequest::KvSet {
|
||||
name,
|
||||
key,
|
||||
value,
|
||||
subject,
|
||||
} => {
|
||||
geth_kv::validate_kv_name(&name).map_err(|_| NodeError::InvalidKvName(name.clone()))?;
|
||||
geth_kv::validate_kv_key(&key).map_err(|_| NodeError::InvalidKvKey(key.clone()))?;
|
||||
let kv = store
|
||||
.get_kv_store_by_name(&name)?
|
||||
.ok_or_else(|| NodeError::KvNotFound(name.clone()))?;
|
||||
ensure_kv_write_authorized(&store, node, subject, &kv.resource_id, &key)?;
|
||||
let stored = StoredKvEntry {
|
||||
kv_id: kv.kv_id,
|
||||
key,
|
||||
|
|
@ -1321,6 +1329,38 @@ fn ensure_resource_exists(store: &Store, resource_id: &str) -> Result<(), NodeEr
|
|||
}
|
||||
}
|
||||
|
||||
fn ensure_kv_write_authorized(
|
||||
store: &Store,
|
||||
node: &LocalNode,
|
||||
subject: Option<String>,
|
||||
resource_id: &str,
|
||||
key: &str,
|
||||
) -> Result<(), NodeError> {
|
||||
let Some(subject) = subject else {
|
||||
return Ok(());
|
||||
};
|
||||
if subject == node.node_id || subject == node.agent_id {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let capability = format!("kv.write_key:{key}");
|
||||
let ops = load_auth_ops_for_resource(store, resource_id)?;
|
||||
let explanation = geth_auth::explain_auth_ops(
|
||||
&ops,
|
||||
PrincipalId::new(subject.clone()),
|
||||
ResourceId::new(resource_id.to_owned()),
|
||||
Capability::new(capability.clone()),
|
||||
);
|
||||
if explanation.allowed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(NodeError::Unauthorized(format!(
|
||||
"{subject} lacks {capability} on {resource_id}: {}",
|
||||
explanation.reason
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn create_resource_secret(
|
||||
store: &Store,
|
||||
resource_id: &str,
|
||||
|
|
|
|||
|
|
@ -795,6 +795,7 @@ fn kv_create_set_get_use_local_store() {
|
|||
name: "prefs".to_owned(),
|
||||
key: "apps/foo/theme".to_owned(),
|
||||
value: "dark".to_owned(),
|
||||
subject: None,
|
||||
},
|
||||
)
|
||||
.expect("set kv");
|
||||
|
|
@ -837,6 +838,51 @@ fn kv_create_set_get_use_local_store() {
|
|||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
|
||||
assert!(
|
||||
geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::KvSet {
|
||||
name: "prefs".to_owned(),
|
||||
key: "apps/foo/accent".to_owned(),
|
||||
value: "blue".to_owned(),
|
||||
subject: Some("node:tablet".to_owned()),
|
||||
},
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::AuthGrant {
|
||||
subject: "node:tablet".to_owned(),
|
||||
resource: "resource:kv:prefs".to_owned(),
|
||||
capability: "kv.write_prefix:apps/foo/".to_owned(),
|
||||
grant_id: None,
|
||||
},
|
||||
)
|
||||
.expect("grant prefix write");
|
||||
geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::KvSet {
|
||||
name: "prefs".to_owned(),
|
||||
key: "apps/foo/accent".to_owned(),
|
||||
value: "blue".to_owned(),
|
||||
subject: Some("node:tablet".to_owned()),
|
||||
},
|
||||
)
|
||||
.expect("authorized prefix write");
|
||||
assert!(
|
||||
geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::KvSet {
|
||||
name: "prefs".to_owned(),
|
||||
key: "apps/bar/accent".to_owned(),
|
||||
value: "red".to_owned(),
|
||||
subject: Some("node:tablet".to_owned()),
|
||||
},
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(
|
||||
geth_node::handle_request(
|
||||
&node,
|
||||
|
|
@ -853,6 +899,7 @@ fn kv_create_set_get_use_local_store() {
|
|||
name: "missing".to_owned(),
|
||||
key: "apps/foo/theme".to_owned(),
|
||||
value: "dark".to_owned(),
|
||||
subject: None,
|
||||
},
|
||||
)
|
||||
.is_err()
|
||||
|
|
|
|||
Loading…
Reference in a new issue