add external gitlab accounts: stored in bao, linked per agent, allowed in squid, usable with glab
This commit is contained in:
parent
3b8ab17240
commit
d6e983995d
1 changed files with 1322 additions and 51 deletions
|
|
@ -1,14 +1,16 @@
|
|||
//! The hosts an agent's linked accounts reach, kept as derived entries of the
|
||||
//! egress allow list (`crate::egress_allow`), so a restricted agent with a
|
||||
//! linked GitHub, external matrix or external forge account reaches that
|
||||
//! service through its hive's proxy.
|
||||
//! linked GitHub, external matrix, external forge or GitLab account reaches
|
||||
//! that service through its hive's proxy.
|
||||
//!
|
||||
//! An entry is marked `derived` and carries the account as its `source`
|
||||
//! (`link:github`, `link:matrix:<account>`, `link:forge:<label>`), so the
|
||||
//! backend environment's entries (`crate::backend_egress`) and each account's
|
||||
//! are replaced and dropped independently. A hive renders it like any other.
|
||||
//! (`link:github`, `link:matrix:<account>`, `link:forge:<label>`,
|
||||
//! `link:gitlab:<label>`), so
|
||||
//! the backend environment's entries (`crate::backend_egress`) and each
|
||||
//! account's are replaced and dropped independently. A hive renders it like
|
||||
//! any other.
|
||||
//!
|
||||
//! The controller alone writes the three leaves the entries come from; the
|
||||
//! The controller alone writes the four leaves the entries come from; the
|
||||
//! agent's store policy grants it `create` on its own `backend-env` leaf and
|
||||
//! nothing else (`swarm-secret-client/src/policy.rs`), so what these leaves
|
||||
//! hold is the operator's choice, unlike the backend environment's.
|
||||
|
|
@ -55,6 +57,10 @@ pub(crate) enum Link<'a> {
|
|||
label: &'a str,
|
||||
url: &'a str,
|
||||
},
|
||||
Gitlab {
|
||||
label: &'a str,
|
||||
url: &'a str,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) fn github_source() -> String {
|
||||
|
|
@ -69,12 +75,17 @@ pub(crate) fn forge_source(label: &str) -> String {
|
|||
format!("{LINK_SOURCE_PREFIX}forge:{label}")
|
||||
}
|
||||
|
||||
pub(crate) fn gitlab_source(label: &str) -> String {
|
||||
format!("{LINK_SOURCE_PREFIX}gitlab:{label}")
|
||||
}
|
||||
|
||||
impl Link<'_> {
|
||||
fn source(&self) -> String {
|
||||
match self {
|
||||
Self::Github => github_source(),
|
||||
Self::Matrix { account, .. } => matrix_source(account),
|
||||
Self::Forge { label, .. } => forge_source(label),
|
||||
Self::Gitlab { label, .. } => gitlab_source(label),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +98,7 @@ impl Link<'_> {
|
|||
let hosts: Vec<Option<&str>> = match self {
|
||||
Self::Github => GITHUB_HOSTS.iter().map(|h| Some(*h)).collect(),
|
||||
Self::Matrix { homeserver, .. } => homeserver.iter().map(|u| host_of(u)).collect(),
|
||||
Self::Forge { url, .. } => vec![host_of(url)],
|
||||
Self::Forge { url, .. } | Self::Gitlab { url, .. } => vec![host_of(url)],
|
||||
};
|
||||
let mut entries = Vec::new();
|
||||
for host in hosts {
|
||||
|
|
@ -221,6 +232,13 @@ async fn wanted_entries(
|
|||
},
|
||||
None => continue,
|
||||
},
|
||||
AccountKind::Gitlab => match account.host.as_deref() {
|
||||
Some(url) => Link::Gitlab {
|
||||
label: &account.name,
|
||||
url,
|
||||
},
|
||||
None => continue,
|
||||
},
|
||||
AccountKind::Matrix | AccountKind::Backend => continue,
|
||||
};
|
||||
entries.extend(link.entries(agent, base));
|
||||
|
|
@ -370,6 +388,18 @@ mod tests {
|
|||
forge.entries("atlas", &base()),
|
||||
[entry("forge.example.org", "atlas", Some("link:forge:oss"))]
|
||||
);
|
||||
let gitlab = Link::Gitlab {
|
||||
label: "work",
|
||||
url: "https://GitLab.Example.org:8443/sub",
|
||||
};
|
||||
assert_eq!(
|
||||
gitlab.entries("atlas", &base()),
|
||||
[entry(
|
||||
"gitlab.example.org",
|
||||
"atlas",
|
||||
Some("link:gitlab:work")
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -385,6 +415,8 @@ mod tests {
|
|||
] {
|
||||
let forge = Link::Forge { label: "x", url };
|
||||
assert_eq!(hosts(&forge), Vec::<String>::new(), "{url:?}");
|
||||
let gitlab = Link::Gitlab { label: "x", url };
|
||||
assert_eq!(hosts(&gitlab), Vec::<String>::new(), "{url:?}");
|
||||
}
|
||||
// Controls: a public name gets one, and a matrix account with no
|
||||
// homeserver has none to derive.
|
||||
|
|
@ -393,6 +425,11 @@ mod tests {
|
|||
url: "https://forge.example.org",
|
||||
};
|
||||
assert_eq!(hosts(&forge), ["forge.example.org"]);
|
||||
let gitlab = Link::Gitlab {
|
||||
label: "x",
|
||||
url: "https://gitlab.example.org",
|
||||
};
|
||||
assert_eq!(hosts(&gitlab), ["gitlab.example.org"]);
|
||||
let matrix = Link::Matrix {
|
||||
account: "work",
|
||||
homeserver: None,
|
||||
|
|
@ -475,6 +512,22 @@ mod tests {
|
|||
Path(("pr1ma".to_owned(), parts.to_owned()))
|
||||
}
|
||||
|
||||
/// Link `atlas`'s GitLab account `work` through the route.
|
||||
async fn link_gitlab(state: &AppState) {
|
||||
let _ = crate::gitlab_account::put_gitlab_account(
|
||||
State(state.clone()),
|
||||
Path(("pr1ma".to_owned(), "atlas".to_owned(), "work".to_owned())),
|
||||
axum::Json(
|
||||
serde_json::from_value(
|
||||
json!({"url": "https://gitlab.example.org:8443/", "token": "t"}),
|
||||
)
|
||||
.expect("a body"),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.expect("linked");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn linking_and_unlinking_through_the_routes_moves_the_entries() {
|
||||
let nats = TestNats::start();
|
||||
|
|
@ -501,6 +554,7 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.expect("linked");
|
||||
link_gitlab(&state).await;
|
||||
let _ = crate::matrix_account::put_matrix_account(
|
||||
State(state.clone()),
|
||||
Path(("pr1ma".to_owned(), "atlas".to_owned(), "work".to_owned())),
|
||||
|
|
@ -514,9 +568,10 @@ mod tests {
|
|||
.await
|
||||
.expect("linked");
|
||||
let forge = entry("forge.example.org", "atlas", Some("link:forge:oss"));
|
||||
let gitlab = entry("gitlab.example.org", "atlas", Some("link:gitlab:work"));
|
||||
let matrix = entry("matrix.example.org", "atlas", Some("link:matrix:work"));
|
||||
let mut all = github_entries("atlas");
|
||||
all.extend([forge.clone(), matrix.clone()]);
|
||||
all.extend([forge.clone(), gitlab.clone(), matrix.clone()]);
|
||||
assert_eq!(list_of(&client).await, sorted(all));
|
||||
|
||||
// The backend environment's own save and delete leave them alone.
|
||||
|
|
@ -535,14 +590,14 @@ mod tests {
|
|||
crate::linked_accounts::delete_backend_env(State(state.clone()), path("atlas"))
|
||||
.await
|
||||
.expect("deleted");
|
||||
assert_eq!(list_of(&client).await.len(), 2 + GITHUB_HOSTS.len());
|
||||
assert_eq!(list_of(&client).await.len(), 3 + GITHUB_HOSTS.len());
|
||||
|
||||
crate::linked_accounts::delete_github_account(State(state.clone()), path("atlas"))
|
||||
.await
|
||||
.expect("deleted");
|
||||
assert_eq!(
|
||||
list_of(&client).await,
|
||||
sorted(vec![forge.clone(), matrix.clone()])
|
||||
sorted(vec![forge.clone(), gitlab.clone(), matrix.clone()])
|
||||
);
|
||||
crate::linked_accounts::delete_forge_account(
|
||||
State(state.clone()),
|
||||
|
|
@ -550,6 +605,16 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.expect("deleted");
|
||||
assert_eq!(
|
||||
list_of(&client).await,
|
||||
sorted(vec![gitlab.clone(), matrix.clone()])
|
||||
);
|
||||
crate::linked_accounts::delete_gitlab_account(
|
||||
State(state.clone()),
|
||||
Path(("pr1ma".to_owned(), "atlas".to_owned(), "work".to_owned())),
|
||||
)
|
||||
.await
|
||||
.expect("deleted");
|
||||
assert_eq!(list_of(&client).await, [matrix]);
|
||||
crate::linked_accounts::delete_matrix_account(
|
||||
State(state.clone()),
|
||||
|
|
@ -571,6 +636,10 @@ mod tests {
|
|||
"swarm/agents/atlas/matrix/work",
|
||||
json!({"value": "t", "homeserver": "https://matrix.example.org"}),
|
||||
);
|
||||
let store = store.with(
|
||||
"swarm/agents/atlas/gitlab/work",
|
||||
json!({"value": "t", "url": "https://gitlab.example.org:8443"}),
|
||||
);
|
||||
// The agent's own account has a homeserver of its own and no entry.
|
||||
let store = store.with(
|
||||
"swarm/agents/atlas/matrix/main",
|
||||
|
|
@ -583,6 +652,7 @@ mod tests {
|
|||
// An orphan: iris has no leaf for it.
|
||||
entry("github.com", "iris", Some("link:github")),
|
||||
// An orphan on a live agent whose forge leaf is gone.
|
||||
entry("gitlab.example.org", "atlas", Some("link:gitlab:old")),
|
||||
entry("forge.example.org", "atlas", Some("link:forge:old")),
|
||||
entry("llm.example.com", "atlas", Some("backend-env")),
|
||||
entry("crates.io", "atlas", None),
|
||||
|
|
@ -596,6 +666,7 @@ mod tests {
|
|||
assert_eq!(reconcile(&client, &store, &live).await.expect("pass"), 0);
|
||||
let mut want = github_entries("atlas");
|
||||
want.extend([
|
||||
entry("gitlab.example.org", "atlas", Some("link:gitlab:work")),
|
||||
entry("matrix.example.org", "atlas", Some("link:matrix:work")),
|
||||
entry("llm.example.com", "atlas", Some("backend-env")),
|
||||
entry("crates.io", "atlas", None),
|
||||
|
|
@ -607,6 +678,38 @@ mod tests {
|
|||
assert_eq!(list_of(&client).await, sorted(want));
|
||||
}
|
||||
|
||||
/// The host of a GitLab entry is the controller-written leaf's `url`. The
|
||||
/// one object an agent can create in the store is its `backend-env` leaf,
|
||||
/// and a GitLab host named there yields no `link:gitlab` entry.
|
||||
#[tokio::test]
|
||||
async fn a_gitlab_entry_comes_from_the_controller_leaf_and_nothing_an_agent_writes() {
|
||||
let agent_written = FakeStore::default().with(
|
||||
"swarm/agents/atlas/backend-env",
|
||||
json!({"env": "GITLAB_HOST=https://evil.example.com\nANTHROPIC_BASE_URL=https://evil.example.com\n"}),
|
||||
);
|
||||
let got = wanted_entries(&agent_written, "atlas", &base())
|
||||
.await
|
||||
.expect("store answers");
|
||||
assert_eq!(got, []);
|
||||
|
||||
// The control: the controller's leaf yields the entry.
|
||||
let leaf = agent_written.with(
|
||||
"swarm/agents/atlas/gitlab/work",
|
||||
json!({"value": "t", "url": "https://gitlab.example.org:8443/sub"}),
|
||||
);
|
||||
let got = wanted_entries(&leaf, "atlas", &base())
|
||||
.await
|
||||
.expect("store answers");
|
||||
assert_eq!(
|
||||
got,
|
||||
[entry(
|
||||
"gitlab.example.org",
|
||||
"atlas",
|
||||
Some("link:gitlab:work")
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_store_error_prunes_nothing_and_adds_nothing() {
|
||||
let nats = TestNats::start();
|
||||
|
|
|
|||
Loading…
Reference in a new issue