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
128 lines
4 KiB
Rust
128 lines
4 KiB
Rust
//! Making an existing human forge account a site admin: the whole job of
|
|
//! `POST /api/forge/users/{name}/admin`, which `swarmctl forge make-admin`
|
|
//! calls.
|
|
//!
|
|
//! Never creates the account. A human's account is made by their first
|
|
//! authelia login to the forge (`oauth2_client` in
|
|
//! `nix/host-modules/hive-forge/default.nix`), so a missing one means that
|
|
//! login has not happened yet, and making it here would be a second way in.
|
|
//!
|
|
//! The decision is pure ([`plan`]), so the tests pin it; the IO on either side
|
|
//! only reads or acts. Same split as [`super::agent_token`].
|
|
|
|
use anyhow::{Context, Result};
|
|
use forgejo_api::structs::{EditUserOption, User};
|
|
|
|
use super::Client;
|
|
use super::agent_token::is_not_found;
|
|
|
|
/// What one request does about the account.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Plan {
|
|
/// The forge has no user by this name: its owner has not logged in yet.
|
|
NoSuchUser,
|
|
/// Already a site admin: nothing to send.
|
|
AlreadyAdmin,
|
|
/// An ordinary account: make it a site admin.
|
|
Promote,
|
|
}
|
|
|
|
/// Decide from the account as the forge reports it (`None` when there is no
|
|
/// such user).
|
|
pub fn plan(user: Option<&User>) -> Plan {
|
|
match user {
|
|
None => Plan::NoSuchUser,
|
|
Some(u) if u.is_admin == Some(true) => Plan::AlreadyAdmin,
|
|
Some(_) => Plan::Promote,
|
|
}
|
|
}
|
|
|
|
/// The `admin_edit_user` body that sets `admin` and nothing else.
|
|
///
|
|
/// Unlike [`super::repo_creation_lockdown`], no `login_name` + `source_id`:
|
|
/// in Forgejo 16 both are optional, and a `source_id` sets the account's login
|
|
/// type (`services/user/update.go`, `UpdateAuth`). `source_id = 0` would turn
|
|
/// an SSO-made account into a local one.
|
|
fn admin_edit() -> EditUserOption {
|
|
EditUserOption {
|
|
active: None,
|
|
admin: Some(true),
|
|
allow_create_organization: None,
|
|
allow_git_hook: None,
|
|
allow_import_local: None,
|
|
description: None,
|
|
email: None,
|
|
full_name: None,
|
|
hide_email: None,
|
|
location: None,
|
|
login_name: None,
|
|
max_repo_creation: None,
|
|
must_change_password: None,
|
|
password: None,
|
|
prohibit_login: None,
|
|
pronouns: None,
|
|
restricted: None,
|
|
source_id: None,
|
|
visibility: None,
|
|
website: None,
|
|
}
|
|
}
|
|
|
|
impl Client {
|
|
/// Make the existing account `name` a site admin, and say what that took.
|
|
/// The caller has already refused an agent's name.
|
|
///
|
|
/// # Errors
|
|
/// When the forge refuses the read or the edit.
|
|
pub async fn make_site_admin(&self, name: &str) -> Result<Plan> {
|
|
let user = match self.api.user_get(name).await {
|
|
Ok(user) => Some(user),
|
|
Err(e) if is_not_found(&e) => None,
|
|
Err(e) => return Err(e).with_context(|| format!("read forge user {name}")),
|
|
};
|
|
let plan = plan(user.as_ref());
|
|
if plan == Plan::Promote {
|
|
self.api
|
|
.admin_edit_user(name, admin_edit())
|
|
.await
|
|
.with_context(|| format!("make forge user {name} a site admin"))?;
|
|
tracing::info!(%name, "swarm forge: made the user a site admin");
|
|
}
|
|
Ok(plan)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn user(is_admin: Option<bool>) -> User {
|
|
serde_json::from_value(serde_json::json!({
|
|
"login": "mara",
|
|
"is_admin": is_admin,
|
|
}))
|
|
.expect("a user decodes")
|
|
}
|
|
|
|
#[test]
|
|
fn a_missing_user_is_not_created() {
|
|
assert_eq!(plan(None), Plan::NoSuchUser);
|
|
}
|
|
|
|
#[test]
|
|
fn an_admin_is_left_alone() {
|
|
assert_eq!(plan(Some(&user(Some(true)))), Plan::AlreadyAdmin);
|
|
}
|
|
|
|
#[test]
|
|
fn an_ordinary_user_is_promoted() {
|
|
assert_eq!(plan(Some(&user(Some(false)))), Plan::Promote);
|
|
}
|
|
|
|
/// No flag in the answer: the edit is sent. On an admin it is a no-op;
|
|
/// skipping it would report success for an ordinary account.
|
|
#[test]
|
|
fn an_unreported_admin_flag_is_promoted() {
|
|
assert_eq!(plan(Some(&user(None))), Plan::Promote);
|
|
}
|
|
}
|