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,28 +1,31 @@
|
|||
//! The accounts linked to one agent, as kind, name and host: what the swarm
|
||||
//! UI's agent panel lists, and deletes.
|
||||
//!
|
||||
//! [`link`] is the write the three link routes share: it refuses a path that
|
||||
//! [`link`] is the write the four link routes share: it refuses a path that
|
||||
//! already holds an account, so replacing one takes a delete first. The
|
||||
//! backend environment route is the exception: it overwrites.
|
||||
//!
|
||||
//! Read from the paths [`crate::matrix_account`], [`crate::forge_account`],
|
||||
//! [`crate::github_account`] and [`crate::backend_env`] write, plus the
|
||||
//! agent's own `main` matrix account, which `matrix_account::agent_token`
|
||||
//! mints into the same directory. Each entry is read for its host and nothing
|
||||
//! else leaves this module: [`LinkedAccount`] has no field a credential could
|
||||
//! land in. A backend environment is listed as set, with no host.
|
||||
//! [`crate::gitlab_account`], [`crate::github_account`] and
|
||||
//! [`crate::backend_env`] write, plus the agent's own `main` matrix account,
|
||||
//! which `matrix_account::agent_token` mints into the same directory. Each
|
||||
//! entry is read for its host and nothing else leaves this module:
|
||||
//! [`LinkedAccount`] has no field a credential could land in. A backend
|
||||
//! environment is listed as set, with no host.
|
||||
//!
|
||||
//! Naming the matrix accounts and forge labels takes `list` on each agent's
|
||||
//! `matrix` and `forge` metadata directories, and deleting an account takes
|
||||
//! `delete` on its metadata path. `controllerPolicyText` in
|
||||
//! `nix/host-modules/swarm-bao.nix` grants both on those paths alone.
|
||||
//! Naming the matrix accounts, forge labels and GitLab labels takes `list` on
|
||||
//! each agent's `matrix`, `forge` and `gitlab` metadata directories, and
|
||||
//! deleting an account takes `delete` on its metadata path.
|
||||
//! `controllerPolicyText` in `nix/host-modules/swarm-bao.nix` grants both on
|
||||
//! those paths alone.
|
||||
//!
|
||||
//! A delete removes the store entry only, and the agent is not told. Its
|
||||
//! GitHub and forge token files stay in place, since the units that write them
|
||||
//! never delete one; its matrix daemon drops the account when it next re-lists.
|
||||
//! A deleted backend environment leaves the agent's copy in `/run` until its
|
||||
//! next boot, and an agent with a non-empty `backendEnvironmentFile` creates
|
||||
//! the leaf again from that file.
|
||||
//! never delete one; its matrix daemon drops the account when it next re-lists,
|
||||
//! and its GitLab fetch drops the account from the `glab` configuration when it
|
||||
//! next runs. A deleted backend environment leaves the agent's copy in `/run`
|
||||
//! until its next boot, and an agent with a non-empty `backendEnvironmentFile`
|
||||
//! creates the leaf again from that file.
|
||||
|
||||
use std::future::Future;
|
||||
|
||||
|
|
@ -31,7 +34,7 @@ use axum::extract::State;
|
|||
use axum::http::StatusCode;
|
||||
use serde::de::{DeserializeOwned, IgnoredAny};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use swarm_secret_client::{Error, SecretStore, backend_env, forge, github, matrix};
|
||||
use swarm_secret_client::{Error, SecretStore, backend_env, forge, github, gitlab, matrix};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::routes::agents::swarm_hive;
|
||||
|
|
@ -47,6 +50,7 @@ const GITHUB_HOST: &str = "github.com";
|
|||
pub enum AccountKind {
|
||||
Matrix,
|
||||
Forgejo,
|
||||
Gitlab,
|
||||
Github,
|
||||
Backend,
|
||||
}
|
||||
|
|
@ -55,10 +59,11 @@ pub enum AccountKind {
|
|||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, ToSchema)]
|
||||
pub struct LinkedAccount {
|
||||
pub(crate) kind: AccountKind,
|
||||
/// The matrix account name, the forge label, `github`, or `backend-env`.
|
||||
/// The matrix account name, the forge or GitLab label, `github`, or
|
||||
/// `backend-env`.
|
||||
#[schema(example = "main")]
|
||||
pub(crate) name: String,
|
||||
/// The matrix homeserver, the forge base URL, or `github.com`. `None` for a
|
||||
/// The matrix homeserver, the forge or GitLab base URL, or `github.com`. `None` for a
|
||||
/// matrix account stored without a homeserver.
|
||||
#[schema(example = "https://matrix.example.org")]
|
||||
pub(crate) host: Option<String>,
|
||||
|
|
@ -190,7 +195,7 @@ async fn objects(store: &impl AccountStore, dir: &str) -> Result<Vec<String>, Er
|
|||
Ok(keys.into_iter().filter(|k| !k.ends_with('/')).collect())
|
||||
}
|
||||
|
||||
/// Every account the store holds for `agent`: matrix, then forgejo, then
|
||||
/// Every account the store holds for `agent`: matrix, then forgejo, then gitlab, then
|
||||
/// github, then the backend environment.
|
||||
///
|
||||
/// A missing directory lists nothing, and a listed name whose object is gone
|
||||
|
|
@ -230,6 +235,18 @@ pub(crate) async fn linked_accounts(
|
|||
}
|
||||
}
|
||||
|
||||
for label in objects(store, &gitlab::accounts_dir(agent)?).await? {
|
||||
let path = gitlab::account_path(agent, &label)?;
|
||||
if let Some(a) = store.read_optional::<gitlab::Account>(&path).await? {
|
||||
out.push(LinkedAccount {
|
||||
kind: AccountKind::Gitlab,
|
||||
name: label,
|
||||
host: Some(a.url),
|
||||
reserved: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let path = github::account_path(agent)?;
|
||||
if store
|
||||
.read_optional::<github::Credential>(&path)
|
||||
|
|
@ -413,7 +430,7 @@ where
|
|||
remove(store, path).await
|
||||
}
|
||||
|
||||
/// Delete the forge account or GitHub token at `path`.
|
||||
/// Delete the forge account, GitLab account or GitHub token at `path`.
|
||||
async fn remove_plain<T: DeserializeOwned + Send>(
|
||||
store: &impl AccountStore,
|
||||
path: &str,
|
||||
|
|
@ -544,6 +561,53 @@ pub async fn delete_forge_account(
|
|||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// Delete an agent's GitLab account from the store, every version of it.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/hives/{hive}/agents/{agent}/gitlab-accounts/{label}",
|
||||
params(
|
||||
("hive" = String, Path, description = "hive the agent runs on"),
|
||||
("agent" = String, Path, description = "agent the account belongs to"),
|
||||
("label" = String, Path, description = "the account's label"),
|
||||
),
|
||||
responses(
|
||||
(status = 204, description = "deleted"),
|
||||
(status = 400, description = "the agent or label is not an identifier, or the hive is not in this swarm (problem+json)", body = String),
|
||||
(status = 404, description = "no such account is stored (problem+json)", body = String),
|
||||
(status = 500, description = "the store could not be read or written (problem+json)", body = String),
|
||||
),
|
||||
tag = "agents"
|
||||
)]
|
||||
pub async fn delete_gitlab_account(
|
||||
State(state): State<AppState>,
|
||||
axum::extract::Path((hive, agent, label)): axum::extract::Path<(String, String, String)>,
|
||||
) -> Result<StatusCode, problem_details::ProblemDetails> {
|
||||
let hive = swarm_hive(&state, &hive).map_err(|(s, d)| error_problem(s, &d))?;
|
||||
let agent = hive_types::Ident::parse(&agent)
|
||||
.map_err(|reason| error_problem(StatusCode::BAD_REQUEST, reason))?
|
||||
.into_string();
|
||||
let label = hive_types::Ident::parse(&label)
|
||||
.map_err(|reason| error_problem(StatusCode::BAD_REQUEST, reason))?
|
||||
.into_string();
|
||||
let path = gitlab::account_path(&agent, &label)
|
||||
.map_err(|e| error_problem(StatusCode::BAD_REQUEST, &e.to_string()))?;
|
||||
|
||||
let store = state.accounts.open().await.map_err(|e| {
|
||||
tracing::warn!(error = %e, "connecting to the swarm secret store failed");
|
||||
error_problem(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||
})?;
|
||||
remove_plain::<gitlab::Account>(&store, &path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!(%hive, %agent, %label, error = ?e, "deleting the gitlab account failed");
|
||||
e.problem()
|
||||
})?;
|
||||
crate::link_egress::removed(&state, &agent, &crate::link_egress::gitlab_source(&label)).await?;
|
||||
|
||||
tracing::info!(%hive, %agent, %label, "gitlab account deleted");
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// Delete an agent's GitHub token from the store, every version of it.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
|
|
@ -645,7 +709,7 @@ pub(crate) mod tests {
|
|||
AccountKind, AccountStore, LinkedAccount, Removal, linked_accounts, remove_matrix,
|
||||
remove_plain,
|
||||
};
|
||||
use swarm_secret_client::{forge, github};
|
||||
use swarm_secret_client::{forge, github, gitlab};
|
||||
|
||||
/// Objects by path. A list answers the next segment of every path under
|
||||
/// the directory, with a trailing `/` when it goes deeper, as the store
|
||||
|
|
@ -773,6 +837,10 @@ pub(crate) mod tests {
|
|||
"swarm/agents/atlas/forge/myforge",
|
||||
json!({"value": "t0k3n-forge", "url": "https://forge.example.org"}),
|
||||
)
|
||||
.with(
|
||||
"swarm/agents/atlas/gitlab/work",
|
||||
json!({"value": "t0k3n-gl", "url": "https://gitlab.example.org"}),
|
||||
)
|
||||
.with(
|
||||
"swarm/agents/atlas/github-token",
|
||||
json!({"value": "t0k3n-gh"}),
|
||||
|
|
@ -820,6 +888,12 @@ pub(crate) mod tests {
|
|||
Some("https://forge.example.org"),
|
||||
false
|
||||
),
|
||||
row(
|
||||
AccountKind::Gitlab,
|
||||
"work",
|
||||
Some("https://gitlab.example.org"),
|
||||
false
|
||||
),
|
||||
row(AccountKind::Github, "github", Some("github.com"), false),
|
||||
row(AccountKind::Backend, "backend-env", None, false),
|
||||
]
|
||||
|
|
@ -887,6 +961,9 @@ pub(crate) mod tests {
|
|||
remove_plain::<forge::Account>(&store, "swarm/agents/atlas/forge/myforge")
|
||||
.await
|
||||
.expect("stored");
|
||||
remove_plain::<gitlab::Account>(&store, "swarm/agents/atlas/gitlab/work")
|
||||
.await
|
||||
.expect("stored");
|
||||
remove_plain::<github::Credential>(&store, "swarm/agents/atlas/github-token")
|
||||
.await
|
||||
.expect("stored");
|
||||
|
|
@ -898,6 +975,7 @@ pub(crate) mod tests {
|
|||
[
|
||||
"swarm/agents/atlas/matrix/catgirl",
|
||||
"swarm/agents/atlas/forge/myforge",
|
||||
"swarm/agents/atlas/gitlab/work",
|
||||
"swarm/agents/atlas/github-token",
|
||||
"swarm/agents/atlas/backend-env",
|
||||
]
|
||||
|
|
@ -915,6 +993,7 @@ pub(crate) mod tests {
|
|||
)
|
||||
.await,
|
||||
remove_plain::<forge::Account>(&store, "swarm/agents/atlas/forge/myforge").await,
|
||||
remove_plain::<gitlab::Account>(&store, "swarm/agents/atlas/gitlab/work").await,
|
||||
remove_plain::<github::Credential>(&store, "swarm/agents/atlas/github-token").await,
|
||||
remove_plain::<IgnoredAny>(&store, "swarm/agents/atlas/backend-env").await,
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in a new issue