Watch
0
0
Fork
You've already forked hyperhive
0

swarm-controller: make an existing forge user a site admin

POST /api/forge/users/{name}/admin reads the account and, when it is not
already a site admin, sets `admin` with admin_edit_user. It never
creates one: a human's account is made by their first authelia login,
so a missing one answers 404, saying the user has not logged in via SSO
yet. An existing admin is a success with nothing sent.

The edit carries `admin` alone. repo_creation_lockdown's login_name +
source_id = 0 would turn an SSO-made account into a local one: in
Forgejo 16 a source_id sets the login type.

An agent's name is refused, and so is any name when the roster can't be
read: a site admin ignores max_repo_creation, the lockdown that keeps an
agent's token from creating a repo and self-merging in it.

Refs #3782
This commit is contained in:
atlas 2026-09-25 00:04:44 +02:00 • committed by mara
commit f1f59ea165
4 changed files with 407 additions and 5 deletions

View file

@ -34,6 +34,7 @@ use utoipa::ToSchema;
use crate::webhook::DeliveryKind;
pub mod agent_token;
pub mod site_admin;
/// An agent's open config-PR, as [`Client::list_open_config_prs`] reports it
/// and `GET /api/agents/{name}/config-pr` serves it.
@ -1659,4 +1660,78 @@ mod tests {
let body = lockdown_patch(&seen, "alice").expect("no lockdown PATCH sent");
assert_eq!(body["max_repo_creation"], 0);
}
/// Every `PATCH` body the stub saw.
fn patches(seen: &Seen) -> Vec<serde_json::Value> {
seen.lock()
.unwrap()
.iter()
.filter(|(method, _, _)| method == "PATCH")
.map(|(_, _, body)| body.clone())
.collect()
}
/// Only `admin` goes out: a `source_id` would reset an SSO-made
/// account's login type, and the other fields would overwrite the user's
/// own settings.
#[tokio::test]
async fn make_site_admin_sets_admin_and_nothing_else() {
let (client, seen) = stub_forge(
(404, "{}"),
(200, r#"{"login":"mara","is_admin":false}"#),
(200, "{}"),
)
.await;
let plan = client.make_site_admin("mara").await.unwrap();
assert_eq!(plan, site_admin::Plan::Promote);
let bodies = patches(&seen);
assert_eq!(bodies.len(), 1, "{bodies:?}");
let set: Vec<(&String, &serde_json::Value)> = bodies[0]
.as_object()
.unwrap()
.iter()
.filter(|(_, v)| !v.is_null())
.collect();
assert_eq!(set, [(&"admin".to_owned(), &serde_json::Value::Bool(true))]);
}
#[tokio::test]
async fn make_site_admin_leaves_an_admin_alone() {
let (client, seen) = stub_forge(
(404, "{}"),
(200, r#"{"login":"mara","is_admin":true}"#),
(200, "{}"),
)
.await;
let plan = client.make_site_admin("mara").await.unwrap();
assert_eq!(plan, site_admin::Plan::AlreadyAdmin);
assert!(patches(&seen).is_empty());
}
/// A user who has not logged in yet is reported, not created: nothing is
/// sent but the read.
#[tokio::test]
async fn make_site_admin_does_not_create_a_missing_user() {
let (client, seen) = stub_forge(
(201, "{}"),
(404, r#"{"message":"user does not exist"}"#),
(200, "{}"),
)
.await;
let plan = client.make_site_admin("mara").await.unwrap();
assert_eq!(plan, site_admin::Plan::NoSuchUser);
let methods: Vec<String> = seen
.lock()
.unwrap()
.iter()
.map(|(m, _, _)| m.clone())
.collect();
assert_eq!(methods, ["GET"]);
}
}