swarm-controller: refuse a new agent name the forge would reject
Every agent becomes a Forgejo user of the same name, and nothing upstream of `CreateForgeUser` knew what Forgejo refuses: `admin`, `api`, `foo-` or a 41-character name passed name validation and failed one node into provisioning with Forgejo's 422. `nix/reserved-names.nix` gains the 22 reserved usernames of Forgejo v16.0.5 (`models/user/user.go:639-680`) that `[a-z0-9-]` can spell, and the bare `-` (`models/repo/repo.go:67`). The dot and underscore entries are left out, since our charset cannot produce them. The header's admission rule grows a third class — a username the forge refuses — because that is a failure behind the refusal. The shape rules are not literals, so they live in `hive_types::forge_username_violation`: no leading `-`, no `--`, no trailing `-`, at most 40 characters. Beside `is_reserved_name`, not in `Ident::parse`: an `Ident` is also a hive, label, account and subagent name, and parsing runs on every read of an existing name. `create_agent` used to WARN on a reserved name, deliberately: an operator with agents already created under a colliding name would otherwise be unable to re-run creation. That reason is kept, and narrowed to what it protects. A name breaking either rule is now refused with a 400 naming the rule when the name is NOT in the swarm roster, and still only warned about when it is, so re-creating an existing agent keeps working. The roster is read only for a rule-breaking name; when it cannot be read, a new name and an existing one look alike, and this warns as before. The hive-collision warning is unchanged.
This commit is contained in:
parent
6a87b25488
commit
88e571a463
3 changed files with 327 additions and 43 deletions
|
|
@ -64,6 +64,35 @@ pub fn is_reserved_name(name: &str, reserved: &[&str]) -> bool {
|
|||
reserved.contains(&name)
|
||||
}
|
||||
|
||||
/// Forgejo's cap on a username (`MaxSize(40)` on the admin create-user body,
|
||||
/// `modules/structs/admin_user.go:14` in v16.0.5) — tighter than
|
||||
/// [`Ident::MAX_LEN`], so an agent name can be a valid [`Ident`] and still
|
||||
/// never get a forge account.
|
||||
pub const FORGE_USERNAME_MAX_LEN: usize = 40;
|
||||
|
||||
/// The Forgejo username shape rule an [`Ident`]-valid `name` breaks, or
|
||||
/// `None`. Forgejo v16.0.5 `modules/validation/helpers.go:97-108`.
|
||||
///
|
||||
/// The shape half of the forge's refusals; its reserved literals are in
|
||||
/// `nix/reserved-names.nix`. Creation sites only, for the reason given at
|
||||
/// [`is_reserved_name`] — and not inside [`Ident::parse`], because an
|
||||
/// `Ident` is also a hive, label, account and subagent name that never
|
||||
/// becomes a forge user.
|
||||
#[must_use]
|
||||
pub fn forge_username_violation(name: &str) -> Option<&'static str> {
|
||||
if name.starts_with('-') {
|
||||
Some("must not start with '-'")
|
||||
} else if name.contains("--") {
|
||||
Some("must not contain '--'")
|
||||
} else if name.ends_with('-') {
|
||||
Some("must not end with '-'")
|
||||
} else if name.len() > FORGE_USERNAME_MAX_LEN {
|
||||
Some("must be 40 characters or fewer")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A validated hive identifier: 1-63 chars of `[a-z0-9-]`.
|
||||
///
|
||||
/// The single ident type for agent names, forge labels, and matrix / github
|
||||
|
|
@ -156,7 +185,10 @@ impl<'de> serde::Deserialize<'de> for Ident {
|
|||
|
||||
#[cfg(test)]
|
||||
mod ident_tests {
|
||||
use super::{Ident, is_reserved_name, parse_reserved_names};
|
||||
use super::{
|
||||
FORGE_USERNAME_MAX_LEN, Ident, forge_username_violation, is_reserved_name,
|
||||
parse_reserved_names,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn accepts_canonical_shapes() {
|
||||
|
|
@ -240,6 +272,36 @@ mod ident_tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Each Forgejo shape rule is named on its own, so a refusal tells the
|
||||
/// operator which character to change. The 63-char case is an `Ident`
|
||||
/// that is still no forge username: the gap between the two caps.
|
||||
#[test]
|
||||
fn forge_shape_rules_are_each_refused() {
|
||||
let over = "a".repeat(FORGE_USERNAME_MAX_LEN + 1);
|
||||
let longest_ident = "a".repeat(Ident::MAX_LEN);
|
||||
for (bad, rule) in [
|
||||
("-", "must not start with '-'"),
|
||||
("-agent", "must not start with '-'"),
|
||||
("my--agent", "must not contain '--'"),
|
||||
("agent-", "must not end with '-'"),
|
||||
(over.as_str(), "must be 40 characters or fewer"),
|
||||
(longest_ident.as_str(), "must be 40 characters or fewer"),
|
||||
] {
|
||||
assert!(Ident::parse(bad).is_ok(), "{bad:?} must be a valid Ident");
|
||||
assert_eq!(forge_username_violation(bad), Some(rule), "{bad:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The accept arm: without it, a predicate refusing everything passes
|
||||
/// the test above. 40 characters is the boundary, not past it.
|
||||
#[test]
|
||||
fn forge_shape_rules_accept_ordinary_names() {
|
||||
let at_cap = "a".repeat(FORGE_USERNAME_MAX_LEN);
|
||||
for ok in ["my-agent", "a", "v3", "a-b-c", at_cap.as_str()] {
|
||||
assert_eq!(forge_username_violation(ok), None, "{ok:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_and_serde_validates() {
|
||||
let id = Ident::parse("damocles").unwrap();
|
||||
|
|
|
|||
Loading…
Reference in a new issue