Skip to main content

teamcraft/services/requests/
permissions.rs

1//! Who may triage and decide, and what a user may do with a request.
2
3use super::*;
4
5/// Project groups of the affected items (`None` = item without a group).
6pub async fn project_ids(
7    conn: &mut PgConnection,
8    request_id: Uuid,
9) -> AppResult<Vec<Option<Uuid>>> {
10    Ok(sqlx::query_scalar::<_, Option<Uuid>>(
11        "SELECT DISTINCT i.project_id FROM change_request_items cri JOIN items i ON i.id = cri.item_id
12         WHERE cri.request_id = $1",
13    )
14    .bind(request_id)
15    .fetch_all(&mut *conn)
16    .await?)
17}
18
19/// Whether `actor` triages and decides a request on items of these project
20/// groups: admins always, CAD editors who are members of one of them (any CAD
21/// editor while no item is affected yet), viewers never.
22pub fn may_triage(actor: &Actor, projects: &[Option<Uuid>]) -> bool {
23    if actor.is_admin() {
24        return true;
25    }
26    actor.can_edit() && (projects.is_empty() || projects.iter().any(|p| actor.can_edit_project(*p)))
27}
28
29/// What the actor may do with a request – the services enforce the same
30/// rules; the web UI, the API and the desktop app show the matching actions.
31#[derive(Clone, Copy, Debug, Default, Serialize)]
32pub struct Abilities {
33    /// Change title, description, reason, priority, due date and the items.
34    pub edit: bool,
35    pub start_triage: bool,
36    /// Accept, reject or defer.
37    pub decide: bool,
38    pub assign: bool,
39    /// Deferred → in triage.
40    pub reopen: bool,
41    pub withdraw: bool,
42    /// Create or link the change order of an accepted request.
43    pub implement: bool,
44    pub comment: bool,
45    pub attach: bool,
46}
47
48pub fn abilities(actor: &Actor, request: &ChangeRequest, projects: &[Option<Uuid>]) -> Abilities {
49    use RequestStatus::*;
50    let status = status_of(request);
51    let triage = may_triage(actor, projects);
52    let requester = request.requested_by == actor.id;
53    let undecided = matches!(status, New | InTriage);
54    Abilities {
55        edit: status.is_open() && (triage || (requester && undecided)),
56        start_triage: triage && status == New,
57        decide: triage && matches!(status, New | InTriage | Deferred),
58        assign: triage && status.is_open(),
59        reopen: triage && status == Deferred,
60        withdraw: (requester || actor.is_admin()) && undecided,
61        implement: triage && status == Accepted,
62        comment: true,
63        attach: status.is_open(),
64    }
65}
66
67/// Loads the abilities of `actor` on a request (for pages and API responses).
68pub async fn abilities_of(
69    pool: &PgPool,
70    actor: &Actor,
71    request: &ChangeRequest,
72) -> AppResult<Abilities> {
73    let mut conn = pool.acquire().await?;
74    let projects = project_ids(&mut conn, request.id).await?;
75    let mut can = abilities(actor, request, &projects);
76    // a group may keep the requester from deciding their own request
77    if can.decide
78        && request.requested_by == actor.id
79        && crate::services::project_policy::requester_cannot_decide(&mut conn, &projects).await?
80    {
81        can.decide = false;
82    }
83    Ok(can)
84}
85
86pub(super) fn forbidden(request: &ChangeRequest, what: &str) -> AppError {
87    AppError::Forbidden(format!(
88        "Only CAD editors of the affected project groups or an administrator can {what} {}.",
89        request.number
90    ))
91}
92
93pub(super) fn not_now(request: &ChangeRequest) -> AppError {
94    AppError::Conflict(format!(
95        "{} is {}; this action is not possible in that state.",
96        request.number,
97        status_of(request).label()
98    ))
99}