use std::collections::HashMap; use axum::{ Json, Router, extract, http::StatusCode, response::{IntoResponse, Redirect}, routing, }; use non_empty_string::NonEmptyString; use nonempty::NonEmpty; use serde::{Deserialize, Serialize}; use crate::{config::Config, models::authelia, routes::auth, state::State}; #[derive(Debug, Serialize)] struct GroupResponse { users: Vec, } type GroupsResponse = HashMap; pub async fn get_all( _: auth::User, extract::State(config): extract::State, ) -> Result { let users = authelia::UsersFile::load(&config.authelia.user_database) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; let mut groups_response: GroupsResponse = HashMap::new(); for (username, user) in users.iter() { for group in &user.groups { let group_response = groups_response .entry(group.clone()) .or_insert_with(|| GroupResponse { users: Vec::new() }); group_response.users.push(username.clone()); } } Ok(Json(groups_response)) } pub async fn get( _: auth::User, extract::Path(name): extract::Path, extract::State(config): extract::State, ) -> Result { let users = authelia::UsersFile::load(&config.authelia.user_database) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; let group_users = users .iter() .filter_map(|(username, user)| { if user.groups.contains(&name) { Some(username.clone()) } else { None } }) .collect::>(); if group_users.is_empty() { return Err(StatusCode::NOT_FOUND); } Ok(Json(GroupResponse { users: group_users })) } #[derive(Debug, Deserialize)] pub struct GroupCreate { name: NonEmptyString, users: NonEmpty, } pub async fn create( _: auth::User, extract::State(config): extract::State, extract::Json(group_create): extract::Json, ) -> Result { let mut users = authelia::UsersFile::load(&config.authelia.user_database) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; if users .iter() .any(|(_, user)| user.groups.contains(&group_create.name)) { return Err(StatusCode::CONFLICT); } if !group_create .users .iter() .all(|user| users.contains_key(user.as_str())) { return Err(StatusCode::NOT_FOUND); } for user in group_create.users { users .get_mut(user.as_str()) .unwrap() .groups .push(group_create.name.clone()); } Ok(()) } #[derive(Debug, Deserialize)] pub struct GroupUpdate { users: Option>, } pub async fn update( session_user: auth::User, extract::Path(name): extract::Path, extract::State(config): extract::State, extract::Json(group_update): extract::Json, ) -> Result { let mut users = authelia::UsersFile::load(&config.authelia.user_database) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; if !users.iter().any(|(_, user)| user.groups.contains(&name)) { return Err(StatusCode::NOT_FOUND); } let mut logout = false; if let Some(new_users) = group_update.users { for (username, user) in users.iter_mut() { if new_users.contains(username) { if !user.groups.contains(&name) { user.groups.push(name.clone()); } } else { user.groups.retain(|g| g != &name); } if *username == *session_user.username && !user.groups.contains(&config.oauth.admin_group) { logout = true; } } } users .save(&config.authelia.user_database) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; if logout { return Ok(Redirect::to("/api/auth/logout").into_response()); } Ok(().into_response()) } pub async fn delete( _: auth::User, extract::Path(name): extract::Path, extract::State(config): extract::State, ) -> Result { if name == config.oauth.admin_group { return Err(StatusCode::FORBIDDEN); } let mut users = authelia::UsersFile::load(&config.authelia.user_database) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; if !users.iter().any(|(_, user)| user.groups.contains(&name)) { return Err(StatusCode::NOT_FOUND); } for user in users.values_mut() { user.groups.retain(|g| g != &name); } users .save(&config.authelia.user_database) .await .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; Ok(()) } pub fn routes(state: State) -> Router { Router::new() .route("/groups", routing::get(get_all)) .route("/groups/{username}", routing::get(get)) .route("/groups", routing::post(create)) .route("/groups/{username}", routing::put(update)) .route("/groups/{username}", routing::delete(delete)) .with_state(state) }