Initial commit
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
240
src/routes/groups.rs
Normal file
240
src/routes/groups.rs
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user