Files
Oxicloud/src/application/dtos/grant_dto.rs
T

308 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! DTOs for the ReBAC `/api/grants` REST endpoints.
//!
//! The wire shapes are intentionally separate from the domain types
//! (`Subject`, `Resource`, `Permission`, `Grant`) so that domain stays
//! storage-agnostic and DTOs can evolve with the HTTP contract.
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use crate::application::dtos::cursor::{CursorListResponse, CursorQuery, PageCursor};
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
use crate::domain::services::authorization::{Grant, Permission, Resource, Subject};
// ════════════════════════════════════════════════════════════════════════════
// Subject / Resource / Permission DTOs
// ════════════════════════════════════════════════════════════════════════════
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum SubjectTypeDto {
User,
Group,
Token,
External,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct SubjectDto {
#[serde(rename = "type")]
pub kind: SubjectTypeDto,
pub id: Uuid,
}
impl From<SubjectDto> for Subject {
fn from(dto: SubjectDto) -> Self {
match dto.kind {
SubjectTypeDto::User => Subject::User(dto.id),
SubjectTypeDto::Group => Subject::Group(dto.id),
SubjectTypeDto::Token => Subject::Token(dto.id),
SubjectTypeDto::External => Subject::External(dto.id),
}
}
}
impl From<Subject> for SubjectDto {
fn from(s: Subject) -> Self {
let (kind, id) = match s {
Subject::User(id) => (SubjectTypeDto::User, id),
Subject::Group(id) => (SubjectTypeDto::Group, id),
Subject::Token(id) => (SubjectTypeDto::Token, id),
Subject::External(id) => (SubjectTypeDto::External, id),
};
SubjectDto { kind, id }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum ResourceTypeDto {
Folder,
File,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ResourceDto {
#[serde(rename = "type")]
pub kind: ResourceTypeDto,
pub id: Uuid,
}
impl From<ResourceDto> for Resource {
fn from(dto: ResourceDto) -> Self {
match dto.kind {
ResourceTypeDto::Folder => Resource::Folder(dto.id),
ResourceTypeDto::File => Resource::File(dto.id),
}
}
}
impl From<Resource> for ResourceDto {
fn from(r: Resource) -> Self {
let (kind, id) = match r {
Resource::Folder(id) => (ResourceTypeDto::Folder, id),
Resource::File(id) => (ResourceTypeDto::File, id),
};
ResourceDto { kind, id }
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PermissionDto {
Read,
Create,
Share,
Comment,
Delete,
Update,
}
impl From<PermissionDto> for Permission {
fn from(p: PermissionDto) -> Self {
match p {
PermissionDto::Read => Permission::Read,
PermissionDto::Create => Permission::Create,
PermissionDto::Share => Permission::Share,
PermissionDto::Comment => Permission::Comment,
PermissionDto::Delete => Permission::Delete,
PermissionDto::Update => Permission::Update,
}
}
}
impl From<Permission> for PermissionDto {
fn from(p: Permission) -> Self {
match p {
Permission::Read => PermissionDto::Read,
Permission::Create => PermissionDto::Create,
Permission::Share => PermissionDto::Share,
Permission::Comment => PermissionDto::Comment,
Permission::Delete => PermissionDto::Delete,
Permission::Update => PermissionDto::Update,
}
}
}
// ════════════════════════════════════════════════════════════════════════════
// Roles (DTO-layer sugar)
// ════════════════════════════════════════════════════════════════════════════
#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum Role {
Viewer,
//Commenter,
Editor,
//Manager,
Admin,
}
impl Role {
/// Expands a role into its constituent raw permissions. Storage and
/// engine know nothing about roles — the server normalizes here before
/// writing rows.
pub fn expand(self) -> &'static [Permission] {
match self {
Role::Viewer => &[Permission::Read],
/* reserved for future
Role::Commenter => &[Permission::Read, Permission::Comment],
*/
Role::Editor => &[
Permission::Read,
Permission::Comment,
Permission::Create,
Permission::Update,
],
/* reserved for future
Role::Manager => &[
Permission::Read,
Permission::Comment,
Permission::Create,
Permission::Update,
Permission::Share,
],
*/
Role::Admin => &[
Permission::Read,
Permission::Comment,
Permission::Create,
Permission::Update,
Permission::Share,
Permission::Delete,
],
}
}
}
// ════════════════════════════════════════════════════════════════════════════
// Request DTOs
// ════════════════════════════════════════════════════════════════════════════
/// `POST /api/grants` — accepts either `permissions` (explicit) or `role`.
/// Server-side validation requires exactly one of the two to be present.
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateGrantDto {
pub subject: SubjectDto,
pub resource: ResourceDto,
#[serde(default)]
pub permissions: Option<Vec<PermissionDto>>,
#[serde(default)]
pub role: Option<Role>,
}
/// `PUT /api/grants/role` — reconcile a subject's role on a resource.
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateRoleDto {
pub subject: SubjectDto,
pub resource: ResourceDto,
pub role: Role,
}
// ════════════════════════════════════════════════════════════════════════════
// Response DTOs
// ════════════════════════════════════════════════════════════════════════════
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct GrantDto {
pub id: Uuid,
pub subject: SubjectDto,
pub resource: ResourceDto,
pub permission: PermissionDto,
pub granted_by: Uuid,
pub granted_at: chrono::DateTime<chrono::Utc>,
}
impl From<Grant> for GrantDto {
fn from(g: Grant) -> Self {
Self {
id: g.id,
subject: g.subject.into(),
resource: g.resource.into(),
permission: g.permission.into(),
granted_by: g.granted_by,
granted_at: g.granted_at,
}
}
}
// ════════════════════════════════════════════════════════════════════════════
// Shared-with-me DTOs (GET /api/grants/incoming/resources)
// ════════════════════════════════════════════════════════════════════════════
/// Query parameters for `GET /api/grants/incoming/resources`.
///
/// `limit`, `cursor`, and `sort_by` follow the standard [`CursorQuery`]
/// contract. They are declared directly here rather than via
/// `#[serde(flatten)]` because `serde_urlencoded` (Axum's query extractor)
/// does not support flattening.
#[derive(Debug, Deserialize, IntoParams)]
pub struct SharedWithMeQuery {
/// Maximum number of items to return (1–200, default 50).
#[serde(default = "CursorQuery::default_limit")]
pub limit: u32,
/// Opaque cursor from a previous response. Omit to start from the
/// most-recently-granted item.
pub cursor: Option<String>,
/// Sort dimension. Supported values: `"granted_at"` (default),
/// `"granted_by"` (for swimlane grouping).
pub sort_by: Option<String>,
/// Comma-separated resource types to include, e.g. `file,folder`.
/// Omit to return all known types.
pub resource_types: Option<String>,
/// Reverse the sort order. Default `false` (normal order).
/// Must be the same on all pages of the same result set — the cursor
/// carries this flag so the server can validate consistency.
#[serde(default)]
pub reverse: bool,
}
impl SharedWithMeQuery {
/// Returns `limit` clamped to `[1, 200]`.
pub fn limit_clamped(&self) -> usize {
self.limit.clamp(1, 200) as usize
}
/// Decode the optional cursor string. Invalid cursor → start from top.
pub fn decode_cursor<C: PageCursor>(&self) -> Option<C> {
self.cursor.as_deref().and_then(C::decode)
}
}
/// The resource payload for one item in the shared-with-me list.
///
/// The variant is discriminated by `resource_type` on the parent
/// [`SharedWithMeItemDto`]. Serialised as the inner object (no wrapper key)
/// via `#[serde(untagged)]`, so consumers see the file/folder fields directly
/// under the `resource` key.
#[derive(Debug, Serialize, ToSchema)]
#[serde(untagged)]
pub enum ResourceContentDto {
File(FileDto),
Folder(FolderDto),
}
/// One item in the shared-with-me list.
///
/// `resource_type` indicates whether `resource` contains a file or a folder.
/// Using a single `resource` field (instead of nullable `file`/`folder` pairs)
/// makes adding new resource types backward-compatible — only `resource_type`
/// gains a new variant; the wrapper shape stays the same.
#[derive(Debug, Serialize, ToSchema)]
pub struct SharedWithMeItemDto {
pub resource_type: ResourceTypeDto,
/// All permissions the caller holds on this resource (aggregated).
pub permissions: Vec<PermissionDto>,
/// Earliest grant date for this resource.
pub granted_at: chrono::DateTime<chrono::Utc>,
/// UUID of the user who created the (earliest) grant.
pub granted_by: Uuid,
/// Full resource details. Shape is determined by `resource_type`.
pub resource: ResourceContentDto,
}
/// Response for `GET /api/grants/incoming/resources`.
pub type SharedWithMeDto = CursorListResponse<SharedWithMeItemDto>;