feat(group): 1st implementation of Groups

this implements first version (manageable only by admin right now)

    routes:

        GET /api/groups
        List subject groups (paginated). Admin-only.

        POST /api/groups
        Create a new ReBAC subject group. Admin-only. The name must match the RFC 5321 local-part shape and be globally unique (case-insensitive).

        GET /api/groups/search
        Search non-virtual groups by name substring. Authenticated only (no admin role required) — backs the share-dialog recipient autocomplete.

        GET /api/groups/{id}
        Fetch a single group's details. Admin-only.

        DELETE /api/groups/{id}
        Delete a group. Cascades to `subject_group_members` (FK) and to `access_grants` rows referencing this group as a subject. Admin-only.

        PATCH /api/groups/{id}
        Update a group's metadata. Admin-only. v1 only persists name renames.

        GET /api/groups/{id}/effective-members
        List every user transitively reached through this group (members of members of members, etc.). Used by admin / audit tooling. Admin-only.

        GET /api/groups/{id}/members
        List the *direct* members of a group (one level only). Admin-only.

        POST /api/groups/{id}/members
        Add a member to a group. Exactly one of `user_id` / `group_id` must be provided. Adding a group-member runs a write-time cycle check and a nesting-depth check (max 8). Admin-only.

        DELETE /api/groups/{id}/members/group/{gid}
        Remove a nested group-member from a group. Admin-only.

        DELETE /api/groups/{id}/members/user/{uid}
        Remove a user-member from a group. Admin-only.

fix hurl

groups

round

groups
This commit is contained in:
Edouard Vanbelle
2026-05-30 23:35:47 +02:00
parent 41356b6490
commit 09985f8a95
54 changed files with 6421 additions and 145 deletions
+7 -37
View File
@@ -1,7 +1,7 @@
use axum::{
Router,
extract::{Json, Path, Query, State},
http::{HeaderMap, StatusCode, header},
http::{HeaderMap, StatusCode},
response::IntoResponse,
routing::{delete, get, post, put},
};
@@ -12,9 +12,9 @@ use crate::application::dtos::settings_dto::{
TestOidcConnectionDto, TestStorageConnectionDto, UpdateUserActiveDto, UpdateUserQuotaDto,
UpdateUserRoleDto, VerifyMigrationDto,
};
use crate::application::ports::auth_ports::TokenServicePort;
use crate::common::di::AppState;
use crate::interfaces::errors::AppError;
use crate::interfaces::middleware::admin::require_admin;
use std::sync::Arc;
use uuid::Uuid;
@@ -61,42 +61,12 @@ pub fn admin_routes() -> Router<Arc<AppState>> {
}
/// Validate JWT and require admin role. Returns (user_id, role).
///
/// Thin wrapper over the shared `require_admin` middleware helper so this
/// handler keeps a stable signature while the implementation lives next to
/// the new `subject_group_handler` that also needs it.
async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(Uuid, String), AppError> {
let auth = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
let token = headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer ").map(|s| s.to_string()))
.or_else(|| {
crate::interfaces::api::cookie_auth::extract_cookie_value(
headers,
crate::interfaces::api::cookie_auth::ACCESS_COOKIE,
)
})
.ok_or_else(|| AppError::unauthorized("Authorization token required"))?;
let claims = auth
.token_service
.validate_token(&token)
.map_err(|e| AppError::unauthorized(format!("Invalid token: {}", e)))?;
if claims.role != "admin" {
return Err(AppError::new(
StatusCode::FORBIDDEN,
"Admin access required",
"Forbidden",
));
}
Ok((
Uuid::parse_str(&claims.sub)
.map_err(|_| AppError::internal_error("Invalid user ID in token"))?,
claims.role,
))
require_admin(state, headers).await
}
/// GET /api/admin/settings/oidc — get OIDC settings for the admin panel