Use file as source of truth

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-06-07 12:57:10 +01:00
parent ed958a8ed0
commit aa3275e4dc
32 changed files with 424 additions and 1233 deletions

View File

@@ -8,41 +8,37 @@ use axum::{
};
use non_empty_string::NonEmptyString;
use nonempty::NonEmpty;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use crate::{
config::Config,
models::{self, groups::Group},
routes::auth,
state::State,
};
use crate::{config::Config, models::authelia, routes::auth, state::State};
#[derive(Debug, Serialize)]
struct GroupResponse {
users: Vec<String>,
users: Vec<NonEmptyString>,
}
impl From<models::groups::GroupWithUsers> for GroupResponse {
fn from(group: models::groups::GroupWithUsers) -> Self {
Self { users: group.users }
}
}
type GroupsResponse = HashMap<String, GroupResponse>;
type GroupsResponse = HashMap<NonEmptyString, GroupResponse>;
pub async fn get_all(
_: auth::User,
extract::State(pg_pool): extract::State<PgPool>,
extract::State(config): extract::State<Config>,
) -> Result<impl IntoResponse, StatusCode> {
let groups_with_users = models::groups::GroupWithUsers::select(&pg_pool)
let users = authelia::UsersFile::load(&config.authelia.user_database)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
let groups_response = groups_with_users
.into_iter()
.map(|group| (group.name.clone(), GroupResponse::from(group)))
.collect::<GroupsResponse>();
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))
}
@@ -50,102 +46,114 @@ pub async fn get_all(
pub async fn get(
_: auth::User,
extract::Path(name): extract::Path<NonEmptyString>,
extract::State(pg_pool): extract::State<PgPool>,
extract::State(config): extract::State<Config>,
) -> Result<impl IntoResponse, StatusCode> {
let group_with_users = models::groups::GroupWithUsers::select_by_name(&pg_pool, name.as_str())
let users = authelia::UsersFile::load(&config.authelia.user_database)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
.ok_or(StatusCode::NOT_FOUND)?;
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
Ok(Json(GroupResponse::from(group_with_users)))
let group_users = users
.iter()
.filter_map(|(username, user)| {
if user.groups.contains(&name) {
Some(username.clone())
} else {
None
}
})
.collect::<Vec<_>>();
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: Vec<NonEmptyString>,
users: NonEmpty<NonEmptyString>,
}
pub async fn create(
_: auth::User,
extract::State(pg_pool): extract::State<PgPool>,
extract::State(config): extract::State<Config>,
extract::Json(group_create): extract::Json<GroupCreate>,
) -> Result<impl IntoResponse, StatusCode> {
if models::groups::Group::select_by_name(&pg_pool, group_create.name.as_str())
let mut users = authelia::UsersFile::load(&config.authelia.user_database)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
.is_some()
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
if users
.iter()
.any(|(_, user)| user.groups.contains(&group_create.name))
{
return Err(StatusCode::CONFLICT);
}
let users = group_create
if !group_create
.users
.into_iter()
.map(|u| u.to_string())
.collect::<Vec<_>>();
if !models::users::User::all_exist_by_names(&pg_pool, &users)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
.iter()
.all(|user| users.contains_key(user.as_str()))
{
return Err(StatusCode::NOT_FOUND);
}
let group_with_users = models::groups::GroupWithUsers {
name: group_create.name.to_string(),
users,
};
models::groups::GroupWithUsers::insert(&pg_pool, &group_with_users)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
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<Vec<NonEmptyString>>,
users: Option<NonEmpty<NonEmptyString>>,
}
pub async fn update(
session_user: auth::User,
extract::Path(name): extract::Path<NonEmptyString>,
extract::State(pg_pool): extract::State<PgPool>,
extract::State(config): extract::State<Config>,
extract::Json(group_update): extract::Json<GroupUpdate>,
) -> Result<impl IntoResponse, StatusCode> {
let group = models::groups::Group::select_by_name(&pg_pool, name.as_str())
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
.ok_or(StatusCode::NOT_FOUND)?;
let mut logout = false;
if let Some(users) = &group_update.users {
let users = users.iter().map(ToString::to_string).collect::<Vec<_>>();
if !models::users::User::all_exist_by_names(&pg_pool, &users)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
{
return Err(StatusCode::NOT_FOUND);
}
models::intersections::UsersGroups::set_users_for_group(
&pg_pool,
group.name.as_str(),
&users,
)
let mut users = authelia::UsersFile::load(&config.authelia.user_database)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
if name == config.oauth.admin_group && !users.contains(&session_user.username) {
logout = true;
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());
}
@@ -155,20 +163,27 @@ pub async fn update(
pub async fn delete(
_: auth::User,
extract::Path(name): extract::Path<String>,
extract::State(pg_pool): extract::State<PgPool>,
extract::Path(name): extract::Path<NonEmptyString>,
extract::State(config): extract::State<Config>,
) -> Result<impl IntoResponse, StatusCode> {
if name == config.oauth.admin_group {
return Err(StatusCode::FORBIDDEN);
}
let group = models::groups::Group::select_by_name(&pg_pool, &name)
let mut users = authelia::UsersFile::load(&config.authelia.user_database)
.await
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?
.ok_or(StatusCode::NOT_FOUND)?;
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
Group::delete_by_name(&pg_pool, &group.name)
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))?;