feat(project): add authz feature module
zhenyi committed
Added lib/project/features/authz.rs lib/project/features/authz.rs +152 −0 View lib/project/features/authz.rs
@@ -0,0 +1,152 @@
1use db::conn::sea_orm::*;
2use db::project::project_members as member_entity;
3use db::project::project_members::ProjectMemberRole;
4use uuid::Uuid;
5
6use crate::error::{ProjectError, ProjectResult};
7use crate::permission::{
8 CREATE_ISSUE_LINK, DELETE_PROJECT, EDIT_PROJECT, MANAGE_ACTIVITIES, MANAGE_DOCUMENTS,
9 MANAGE_LINKS, MANAGE_MEMBERS, MANAGE_VIEWS, Permissions, STAR, TRANSITION_STATE,
10 VIEW_PROJECT,
11};
12use crate::service::ProjectService;
13
14/// Resolve a user's effective permissions for a project.
15///
16/// Two-tier check:
17/// 1. Caller must be an active space member (delegated to space::features::authz).
18/// 2. Project role is looked up in `project_members` (cached). Non-members get
19/// read-only `Viewer` defaults.
20pub async fn resolve(
21 svc: &ProjectService,
22 user_id: Uuid,
23 space_id: Uuid,
24 project_id: Uuid,
25) -> ProjectResult<Permissions> {
26 let reader = svc.db.reader();
27 if let Err(e) =
28 space::features::authz::effective_permissions(&reader, space_id, user_id).await
29 {
30 return Err(ProjectError::Forbidden(format!(
31 "space membership check failed: {e}"
32 )));
33 }
34
35 let role = lookup_role(svc, project_id, user_id).await?;
36 Ok(role_to_permissions(role))
37}
38
39/// Same as [`resolve`] but caches the role lookup.
40pub async fn resolve_with_cache(
41 svc: &ProjectService,
42 user_id: Uuid,
43 space_id: Uuid,
44 project_id: Uuid,
45) -> ProjectResult<Permissions> {
46 let reader = svc.db.reader();
47 if let Err(e) =
48 space::features::authz::effective_permissions(&reader, space_id, user_id).await
49 {
50 return Err(ProjectError::Forbidden(format!(
51 "space membership check failed: {e}"
52 )));
53 }
54
55 let role = match svc
56 .cache
57 .get_member_role(project_id, user_id)
58 .await
59 .ok()
60 .flatten()
61 {
62 Some(v) => decode_role(v),
63 None => {
64 let fetched = lookup_role(svc, project_id, user_id).await?;
65 let _ = svc
66 .cache
67 .put_member_role(
68 project_id,
69 user_id,
70 fetched as i32,
71 svc.config.cache_ttl_member_role_secs,
72 )
73 .await;
74 fetched
75 }
76 };
77 Ok(role_to_permissions(role))
78}
79
80/// Require a specific permission on a project; short-circuits with Forbidden.
81pub async fn require(
82 svc: &ProjectService,
83 user_id: Uuid,
84 space_id: Uuid,
85 project_id: Uuid,
86 action: &'static str,
87) -> ProjectResult<Permissions> {
88 let perms = resolve(svc, user_id, space_id, project_id).await?;
89 if !perms.has(action) {
90 return Err(ProjectError::Forbidden(format!(
91 "caller lacks permission `{action}` for project {project_id}"
92 )));
93 }
94 Ok(perms)
95}
96
97async fn lookup_role(
98 svc: &ProjectService,
99 project_id: Uuid,
100 user_id: Uuid,
101) -> ProjectResult<ProjectMemberRole> {
102 let reader = svc.db.reader();
103 let row = member_entity::Entity::find()
104 .filter(member_entity::Column::ProjectId.eq(project_id))
105 .filter(member_entity::Column::UserId.eq(user_id))
106 .one(&reader)
107 .await?;
108 Ok(row.map(|m| m.role).unwrap_or(ProjectMemberRole::Viewer))
109}
110
111fn decode_role(v: i32) -> ProjectMemberRole {
112 match v {
113 0 => ProjectMemberRole::Owner,
114 1 => ProjectMemberRole::Lead,
115 2 => ProjectMemberRole::Contributor,
116 _ => ProjectMemberRole::Viewer,
117 }
118}
119
120pub fn role_to_permissions(role: ProjectMemberRole) -> Permissions {
121 match role {
122 ProjectMemberRole::Owner => Permissions::from_vec(&[
123 VIEW_PROJECT,
124 EDIT_PROJECT,
125 DELETE_PROJECT,
126 TRANSITION_STATE,
127 MANAGE_MEMBERS,
128 MANAGE_DOCUMENTS,
129 MANAGE_VIEWS,
130 MANAGE_LINKS,
131 MANAGE_ACTIVITIES,
132 CREATE_ISSUE_LINK,
133 STAR,
134 ]),
135 ProjectMemberRole::Lead => Permissions::from_vec(&[
136 VIEW_PROJECT,
137 EDIT_PROJECT,
138 TRANSITION_STATE,
139 MANAGE_MEMBERS,
140 MANAGE_DOCUMENTS,
141 MANAGE_VIEWS,
142 MANAGE_LINKS,
143 MANAGE_ACTIVITIES,
144 CREATE_ISSUE_LINK,
145 STAR,
146 ]),
147 ProjectMemberRole::Contributor => {
148 Permissions::from_vec(&[VIEW_PROJECT, CREATE_ISSUE_LINK, STAR])
149 }
150 ProjectMemberRole::Viewer => Permissions::from_vec(&[VIEW_PROJECT]),
151 }
152}