add a per-agent egress log mode
This commit is contained in:
parent
9140d063d7
commit
e46ad07226
1 changed files with 637 additions and 31 deletions
|
|
@ -37,14 +37,31 @@ use crate::wanted::WantedWriter;
|
|||
const MAX_ATTEMPTS: usize = 5;
|
||||
|
||||
/// The permissions to store for `req`, whose list is already checked. The list
|
||||
/// replaces the stored one; `unrestricted_internet` left out keeps the one in `current`.
|
||||
/// replaces the stored one; `unrestricted_internet` left out keeps the one in
|
||||
/// `current`. `internet_log` is stored only as `true`, and a request that sets
|
||||
/// `unrestricted_internet` without it turns it off: an agent that moves to
|
||||
/// unrestricted and back to restricted does not come back logging.
|
||||
fn into_stored(req: Permissions, current: &AgentPermissions) -> AgentPermissions {
|
||||
let internet_log = match req.internet_log {
|
||||
Some(on) => on.then_some(true),
|
||||
None if req.unrestricted_internet.is_some() => None,
|
||||
None => current.internet_log,
|
||||
};
|
||||
AgentPermissions {
|
||||
permissions: req.permissions,
|
||||
unrestricted_internet: req.unrestricted_internet.or(current.unrestricted_internet),
|
||||
internet_log,
|
||||
}
|
||||
}
|
||||
|
||||
/// Why `value` is refused: `internet_log` is only meaningful beside an explicit
|
||||
/// `unrestricted_internet: false`, the value an older hive reads.
|
||||
fn log_without_restriction(value: &AgentPermissions) -> Option<&'static str> {
|
||||
(value.internet_log == Some(true) && value.unrestricted_internet != Some(false)).then_some(
|
||||
"internet_log needs unrestricted_internet to be false, in this request or stored",
|
||||
)
|
||||
}
|
||||
|
||||
/// Refuse a list that names a permission twice.
|
||||
pub(crate) fn unique(perms: &[Permission]) -> Result<(), String> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
|
|
@ -62,6 +79,7 @@ fn view(v: AgentPermissions) -> Permissions {
|
|||
Permissions {
|
||||
permissions: v.permissions,
|
||||
unrestricted_internet: v.unrestricted_internet,
|
||||
internet_log: v.internet_log,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +191,9 @@ pub(crate) async fn put_agent_permissions(
|
|||
.await
|
||||
.map_err(|e| queue_problem(&e))?;
|
||||
let value = into_stored(req.clone(), ¤t);
|
||||
if let Some(why) = log_without_restriction(&value) {
|
||||
return Err(refuse(error_problem(StatusCode::BAD_REQUEST, why)));
|
||||
}
|
||||
if (current == value && !undecodable)
|
||||
|| write(&store, &agent, &value, revision)
|
||||
.await
|
||||
|
|
@ -214,10 +235,12 @@ pub(crate) async fn delete_agent_permissions(
|
|||
let (current, revision, _) = read_or_default::<AgentPermissions>(&store, &agent)
|
||||
.await
|
||||
.map_err(|e| queue_problem(&e))?;
|
||||
// Only the flag outlives a clear: dropping it would turn an agent
|
||||
// created with `false` on.
|
||||
// Only the flags outlive a clear: dropping `unrestricted_internet`
|
||||
// would turn an agent created with `false` on, and dropping
|
||||
// `internet_log` would turn a logging agent into an enforced one.
|
||||
let kept = AgentPermissions {
|
||||
unrestricted_internet: current.unrestricted_internet,
|
||||
internet_log: current.internet_log,
|
||||
..AgentPermissions::default()
|
||||
};
|
||||
if kept == AgentPermissions::default() {
|
||||
|
|
@ -307,6 +330,7 @@ pub(crate) fn at_create(
|
|||
Ok(AgentPermissions {
|
||||
permissions: perms,
|
||||
unrestricted_internet,
|
||||
internet_log: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -329,6 +353,13 @@ fn merged_at_create(
|
|||
.unrestricted_internet
|
||||
.or(current.unrestricted_internet)
|
||||
.or_else(|| (!held).then_some(false)),
|
||||
// A create is not a decision about logging: a request that sets
|
||||
// `unrestricted_internet` ends it, as a `PUT` does.
|
||||
internet_log: if value.unrestricted_internet.is_some() {
|
||||
None
|
||||
} else {
|
||||
current.internet_log
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -596,6 +627,80 @@ mod tests {
|
|||
use hive_types::Permission;
|
||||
use swarm_wire::agent_permissions::{Import, ImportReply};
|
||||
|
||||
fn stored(json: &str) -> swarm_wire::agent_permissions::AgentPermissions {
|
||||
serde_json::from_str(json).expect("decodes")
|
||||
}
|
||||
|
||||
fn request(unrestricted_internet: Option<bool>, internet_log: Option<bool>) -> Permissions {
|
||||
Permissions {
|
||||
permissions: None,
|
||||
unrestricted_internet,
|
||||
internet_log,
|
||||
}
|
||||
}
|
||||
|
||||
/// Logging is never switched by a request that does not name it, except
|
||||
/// that setting the main flag ends it, so no agent comes back logging.
|
||||
#[test]
|
||||
fn a_request_changes_logging_only_by_naming_it_or_the_main_flag() {
|
||||
let logging = stored(r#"{"unrestricted_internet":false,"internet_log":true}"#);
|
||||
let after = |req| super::into_stored(req, &logging).internet_log;
|
||||
assert_eq!(after(request(None, None)), Some(true), "left out: kept");
|
||||
assert_eq!(after(request(Some(false), Some(true))), Some(true));
|
||||
assert_eq!(
|
||||
after(request(Some(false), None)),
|
||||
None,
|
||||
"main flag set: ended"
|
||||
);
|
||||
assert_eq!(
|
||||
after(request(Some(true), None)),
|
||||
None,
|
||||
"unrestricted: ended"
|
||||
);
|
||||
assert_eq!(after(request(Some(false), Some(false))), None);
|
||||
let plain = stored(r#"{"unrestricted_internet":false}"#);
|
||||
assert_eq!(
|
||||
super::into_stored(request(None, Some(true)), &plain).internet_log,
|
||||
Some(true),
|
||||
"control: the field turns it on beside a stored false"
|
||||
);
|
||||
}
|
||||
|
||||
/// `true` without a stored or requested `false` is refused; the pair and
|
||||
/// the plain restricted record are not.
|
||||
#[test]
|
||||
fn logging_is_refused_unless_the_main_flag_is_false() {
|
||||
let refused = |current: &str, req| {
|
||||
super::log_without_restriction(&super::into_stored(req, &stored(current))).is_some()
|
||||
};
|
||||
assert!(refused("{}", request(None, Some(true))), "flag absent");
|
||||
assert!(
|
||||
refused("{}", request(Some(true), Some(true))),
|
||||
"unrestricted"
|
||||
);
|
||||
assert!(!refused("{}", request(Some(false), Some(true))), "the pair");
|
||||
assert!(
|
||||
!refused(
|
||||
r#"{"unrestricted_internet":false}"#,
|
||||
request(None, Some(true))
|
||||
),
|
||||
"stored false"
|
||||
);
|
||||
assert!(!refused("{}", request(Some(false), None)), "restricted");
|
||||
}
|
||||
|
||||
/// A create keeps a stored logging flag unless it sets the main flag.
|
||||
#[test]
|
||||
fn a_create_keeps_logging_unless_it_sets_the_main_flag() {
|
||||
let current = stored(r#"{"unrestricted_internet":false,"internet_log":true}"#);
|
||||
let merged = |value: &str| super::merged_at_create(&stored(value), ¤t, true);
|
||||
assert_eq!(merged("{}").internet_log, Some(true));
|
||||
assert_eq!(
|
||||
merged(r#"{"unrestricted_internet":false}"#).internet_log,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
async fn published(client: &async_nats::Client, agent: &str) -> Option<String> {
|
||||
let store = swarm_queue_client::agent_permissions::open_or_create(client)
|
||||
.await
|
||||
|
|
@ -611,6 +716,7 @@ mod tests {
|
|||
Permissions {
|
||||
permissions: Some(vec![Permission::ManageRootAgent]),
|
||||
unrestricted_internet: None,
|
||||
internet_log: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -655,6 +761,7 @@ mod tests {
|
|||
let none = Permissions {
|
||||
permissions: Some(Vec::new()),
|
||||
unrestricted_internet: None,
|
||||
internet_log: None,
|
||||
};
|
||||
let _ = put(&state, "atlas", none.clone()).await.expect("set");
|
||||
assert_eq!(
|
||||
|
|
@ -670,6 +777,53 @@ mod tests {
|
|||
assert_eq!(get(&state, "atlas").await, Permissions::default());
|
||||
}
|
||||
|
||||
/// Through the handlers: `log` is stored beside the old flag at `false`, a
|
||||
/// `DELETE` keeps both, and a `PUT` of the main flag alone ends it.
|
||||
#[tokio::test]
|
||||
async fn log_mode_is_stored_beside_the_old_flag_and_ends_with_the_main_flag() {
|
||||
let nats = TestNats::start();
|
||||
let client = nats.client().await;
|
||||
let state = crate::agent_limits::tests::declared(&client).await;
|
||||
let logging = r#"{"unrestricted_internet":false,"internet_log":true}"#;
|
||||
|
||||
let log = Permissions {
|
||||
permissions: None,
|
||||
unrestricted_internet: Some(false),
|
||||
internet_log: Some(true),
|
||||
};
|
||||
let stored = put(&state, "atlas", log.clone()).await.expect("set").0;
|
||||
assert_eq!(stored, log);
|
||||
assert_eq!(published(&client, "atlas").await.as_deref(), Some(logging));
|
||||
|
||||
let _ = delete_agent_permissions(State(state.clone()), Path("atlas".to_owned())).await;
|
||||
assert_eq!(published(&client, "atlas").await.as_deref(), Some(logging));
|
||||
|
||||
// Refused: `log` beside `true`, and nothing is written.
|
||||
let both = Permissions {
|
||||
unrestricted_internet: Some(true),
|
||||
..log.clone()
|
||||
};
|
||||
let refused = put(&state, "atlas", both).await.expect_err("refused");
|
||||
assert_eq!(refused.status, Some(StatusCode::BAD_REQUEST));
|
||||
assert_eq!(published(&client, "atlas").await.as_deref(), Some(logging));
|
||||
|
||||
// The main flag alone ends it, and `false` again does not bring it back.
|
||||
let on = Permissions {
|
||||
permissions: None,
|
||||
unrestricted_internet: Some(true),
|
||||
internet_log: None,
|
||||
};
|
||||
let _ = put(&state, "atlas", on.clone()).await.expect("set");
|
||||
let off = Permissions {
|
||||
unrestricted_internet: Some(false),
|
||||
..on.clone()
|
||||
};
|
||||
let _ = put(&state, "atlas", off).await.expect("set");
|
||||
assert_eq!(
|
||||
published(&client, "atlas").await.as_deref(),
|
||||
Some(r#"{"unrestricted_internet":false}"#)
|
||||
);
|
||||
}
|
||||
/// An agent stored with `unrestricted_internet: false` keeps it through a
|
||||
/// `PUT` that leaves it out and through `DELETE`; a `PUT` that names it
|
||||
/// changes it. Without the flag, `DELETE` still removes the key.
|
||||
|
|
|
|||
Loading…
Reference in a new issue