teamcraft/services/requests/
permissions.rs1use super::*;
4
5pub 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
19pub 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#[derive(Clone, Copy, Debug, Default, Serialize)]
32pub struct Abilities {
33 pub edit: bool,
35 pub start_triage: bool,
36 pub decide: bool,
38 pub assign: bool,
39 pub reopen: bool,
41 pub withdraw: bool,
42 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
67pub 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 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}