Initial commit

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-06-04 22:50:18 +01:00
commit ec7055d5ff
22 changed files with 5558 additions and 0 deletions

240
src/routes/groups.rs Normal file
View File

@@ -0,0 +1,240 @@
use std::collections::HashMap;
use axum::{
Json, Router, extract,
http::StatusCode,
response::{IntoResponse, Redirect},
routing,
};
use log::error;
use non_empty_string::NonEmptyString;
use serde::{Deserialize, Serialize};
use crate::{models::authelia, routes::auth, state::State};
#[derive(Debug, Serialize)]
struct GroupResponse {
groupname: NonEmptyString,
users: Vec<NonEmptyString>,
}
impl From<(NonEmptyString, authelia::Group)> for GroupResponse {
fn from((groupname, group): (NonEmptyString, authelia::Group)) -> Self {
Self {
groupname,
users: group.users,
}
}
}
type GroupsResponse = HashMap<NonEmptyString, GroupResponse>;
impl From<authelia::Groups> for GroupsResponse {
fn from(groups: authelia::Groups) -> Self {
groups
.groups
.into_iter()
.map(|(key, group)| (key.clone(), GroupResponse::from((key, group))))
.collect()
}
}
pub async fn get_all(
_user: auth::User,
extract::State(state): extract::State<State>,
) -> Result<impl IntoResponse, StatusCode> {
let groups = state.load_groups().map_err(|e| {
error!("Failed to read users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Json(GroupsResponse::from(groups)))
}
pub async fn get(
_user: auth::User,
extract::Path(groupname): extract::Path<NonEmptyString>,
extract::State(state): extract::State<State>,
) -> Result<impl IntoResponse, StatusCode> {
let groups = state.load_groups().map_err(|e| {
error!("Failed to read users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
groups.get(&groupname).cloned().map_or_else(
|| Err(StatusCode::NOT_FOUND),
|group| Ok(Json(GroupResponse::from((groupname, group))).into_response()),
)
}
#[derive(Debug, Deserialize)]
pub struct GroupCreate {
groupname: NonEmptyString,
users: Vec<NonEmptyString>,
}
impl From<GroupCreate> for authelia::Group {
fn from(update: GroupCreate) -> Self {
Self {
users: update.users,
}
}
}
pub async fn create(
_user: auth::User,
extract::State(state): extract::State<State>,
extract::Json(group_create): extract::Json<GroupCreate>,
) -> Result<impl IntoResponse, StatusCode> {
let (mut users, groups) = state.load_users_and_groups().map_err(|e| {
error!("Failed to read users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let groupname = group_create.groupname.clone();
if groups.contains_key(&groupname) {
return Err(StatusCode::CONFLICT);
}
let group_created = authelia::Group::from(group_create);
for username in &group_created.users {
if !users.contains_key(username) {
return Err(StatusCode::NOT_FOUND);
}
users
.get_mut(username)
.unwrap()
.groups
.push(groupname.clone());
}
state.save_users(users).map_err(|e| {
error!("Failed to save users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Json(GroupResponse::from((groupname, group_created))).into_response())
}
#[derive(Debug, Deserialize)]
pub struct GroupUpdate {
groupname: Option<NonEmptyString>,
users: Vec<NonEmptyString>,
}
impl From<GroupUpdate> for authelia::Group {
fn from(update: GroupUpdate) -> Self {
Self {
users: update.users,
}
}
}
pub async fn update(
user: auth::User,
extract::Path(groupname): extract::Path<NonEmptyString>,
extract::State(state): extract::State<State>,
extract::Json(group_update): extract::Json<GroupUpdate>,
) -> Result<impl IntoResponse, StatusCode> {
let (mut users, groups) = state.load_users_and_groups().map_err(|e| {
error!("Failed to read users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let new_groupname = group_update
.groupname
.clone()
.unwrap_or_else(|| groupname.clone());
let group_existing = groups.get(&groupname).ok_or(StatusCode::NOT_FOUND)?;
let group_updated = authelia::Group::from(group_update);
if groupname != new_groupname
&& (groupname == state.config.oauth.admin_group
|| new_groupname == state.config.oauth.admin_group)
{
return Err(StatusCode::FORBIDDEN);
}
if groupname != new_groupname && groups.contains_key(&new_groupname) {
return Err(StatusCode::CONFLICT);
}
for user in &group_existing.users {
let user = users.get_mut(user).unwrap();
let pos = user.groups.iter().position(|g| g == &groupname).unwrap();
user.groups.remove(pos);
}
for username in &group_updated.users {
if !users.contains_key(username) {
return Err(StatusCode::NOT_FOUND);
}
let user = users.get_mut(username).unwrap();
if !user.groups.contains(&new_groupname) {
user.groups.push(new_groupname.clone());
}
}
state.save_users(users).map_err(|e| {
error!("Failed to save users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
if new_groupname == state.config.oauth.admin_group
&& !group_updated
.users
.iter()
.any(|group_user| *group_user == *user.username.to_string())
{
return Ok(Redirect::to("/api/auth/logout").into_response());
}
Ok(Json(GroupResponse::from((new_groupname, group_updated))).into_response())
}
pub async fn delete(
_user: auth::User,
extract::Path(groupname): extract::Path<String>,
extract::State(state): extract::State<State>,
) -> Result<impl IntoResponse, StatusCode> {
let (mut users, groups) = state.load_users_and_groups().map_err(|e| {
error!("Failed to read users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
if groupname == state.config.oauth.admin_group {
return Err(StatusCode::FORBIDDEN);
}
if let Some(old_group) = groups.get(&groupname) {
for user in &old_group.users {
let user = users.get_mut(user).unwrap();
let pos = user.groups.iter().position(|g| g == &groupname).unwrap();
user.groups.remove(pos);
}
} else {
return Err(StatusCode::NOT_FOUND);
}
state.save_users(users).map_err(|e| {
error!("Failed to save users file: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(StatusCode::NO_CONTENT.into_response())
}
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)
}