swarm-controller: make authelia's operators matrix homeserver admins
swarm-authelia-bridge gains ListOperators, the usernames in authelia's operator group (`operatorGroup = "admins"`, passed in from swarm-authelia.nix). Unlike the two agent verbs, which admit any active token, it answers only the client named by SWARM_AUTHELIA_BRIDGE_OPERATOR_READER_CLIENT_ID (swarm-controller's queueClientId) and returns 403 to every other active token: authelia issues machine tokens to every hive and its agents as well. The bridge's introspection now models `client_id` to make that check, the same way swarm-nats-auth's does. swarm-controller runs a grant-only pass at start and every five minutes: read the group, map each username to the account an SSO login lands on (tuwunel lowercases `preferred_username`), skip anyone already joined to `#admins:<server>` (tuwunel's definition of admin) or without a matrix account, and send `!admin users make-user-admin <user>` there as @swarm with the swarm appservice token. Nothing is ever revoked: tuwunel has no command for it (#4799). The admin-room calls (alias, joined members, profile, send) go into swarm-matrix-client next to whoami, keeping its no-body-in-errors rule. Closes #4585
This commit is contained in:
parent
2c1a9fa488
commit
9e7fdb55ae
15 changed files with 787 additions and 33 deletions
|
|
@ -132,10 +132,16 @@ pub fn mark_as_agent(store: &mut UserStore, name: &str) -> bool {
|
|||
/// Sorted because [`UserStore::users`] is a `BTreeMap`, so this is free and
|
||||
/// a consumer diffing two answers sees real changes rather than reordering.
|
||||
pub fn agent_names(store: &UserStore) -> Vec<String> {
|
||||
group_members(store, AGENT_GROUP)
|
||||
}
|
||||
|
||||
/// The names in the store that carry `group`, sorted for
|
||||
/// [`agent_names`]'s reason.
|
||||
pub fn group_members(store: &UserStore, group: &str) -> Vec<String> {
|
||||
store
|
||||
.users
|
||||
.iter()
|
||||
.filter(|(_, user)| user.groups.iter().any(|g| g == AGENT_GROUP))
|
||||
.filter(|(_, user)| user.groups.iter().any(|g| g == group))
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -343,9 +349,9 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// The group an operator carries — a literal here rather than a shared
|
||||
/// constant, because this binary does not read the operator group and a
|
||||
/// test that imported one would imply it did.
|
||||
/// The group an operator carries — a literal here, because this binary
|
||||
/// takes the operator group from its environment and has no constant to
|
||||
/// import.
|
||||
fn operator(password: &str) -> User {
|
||||
User {
|
||||
groups: vec!["admins".to_owned()],
|
||||
|
|
@ -477,6 +483,22 @@ some_future_top_level_key: 7
|
|||
assert_eq!(agent_names(&store), ["atlas"]);
|
||||
}
|
||||
|
||||
/// The operator list is the group's members only — not agents, and not
|
||||
/// an account that carries no group.
|
||||
#[test]
|
||||
fn the_operator_list_is_the_groups_members_and_not_the_file() {
|
||||
let mut store = UserStore::default();
|
||||
store.users.insert("atlas".to_owned(), agent("$argon2id$a"));
|
||||
store
|
||||
.users
|
||||
.insert("mara".to_owned(), operator("$argon2id$b"));
|
||||
store
|
||||
.users
|
||||
.insert("ungrouped".to_owned(), user("$argon2id$c"));
|
||||
|
||||
assert_eq!(group_members(&store, "admins"), ["mara"]);
|
||||
}
|
||||
|
||||
/// An identity that predates the marker is invisible to the roster until
|
||||
/// agent creation runs again — which is the agreed migration, so it has
|
||||
/// to actually change something.
|
||||
|
|
|
|||
Loading…
Reference in a new issue