use db::conn::sea_orm::*; use db::project::project_members as member_entity; use db::project::project_members::ProjectMemberRole; use uuid::Uuid; use crate::error::{ProjectError, ProjectResult}; use crate::permission::{ CREATE_ISSUE_LINK, DELETE_PROJECT, EDIT_PROJECT, MANAGE_ACTIVITIES, MANAGE_DOCUMENTS, MANAGE_LINKS, MANAGE_MEMBERS, MANAGE_VIEWS, Permissions, STAR, TRANSITION_STATE, VIEW_PROJECT, }; use crate::service::ProjectService; /// Resolve a user's effective permissions for a project. /// /// Two-tier check: /// 1. Caller must be an active space member (delegated to space::features::authz). /// 2. Project role is looked up in `project_members` (cached). Non-members get /// read-only `Viewer` defaults. pub async fn resolve( svc: &ProjectService, user_id: Uuid, space_id: Uuid, project_id: Uuid, ) -> ProjectResult { let reader = svc.db.reader(); if let Err(e) = space::features::authz::effective_permissions(&reader, space_id, user_id).await { return Err(ProjectError::Forbidden(format!( "space membership check failed: {e}" ))); } let role = lookup_role(svc, project_id, user_id).await?; Ok(role_to_permissions(role)) } /// Same as [`resolve`] but caches the role lookup. pub async fn resolve_with_cache( svc: &ProjectService, user_id: Uuid, space_id: Uuid, project_id: Uuid, ) -> ProjectResult { let reader = svc.db.reader(); if let Err(e) = space::features::authz::effective_permissions(&reader, space_id, user_id).await { return Err(ProjectError::Forbidden(format!( "space membership check failed: {e}" ))); } let role = match svc .cache .get_member_role(project_id, user_id) .await .ok() .flatten() { Some(v) => decode_role(v), None => { let fetched = lookup_role(svc, project_id, user_id).await?; let _ = svc .cache .put_member_role( project_id, user_id, fetched as i32, svc.config.cache_ttl_member_role_secs, ) .await; fetched } }; Ok(role_to_permissions(role)) } /// Require a specific permission on a project; short-circuits with Forbidden. pub async fn require( svc: &ProjectService, user_id: Uuid, space_id: Uuid, project_id: Uuid, action: &'static str, ) -> ProjectResult { let perms = resolve(svc, user_id, space_id, project_id).await?; if !perms.has(action) { return Err(ProjectError::Forbidden(format!( "caller lacks permission `{action}` for project {project_id}" ))); } Ok(perms) } async fn lookup_role( svc: &ProjectService, project_id: Uuid, user_id: Uuid, ) -> ProjectResult { let reader = svc.db.reader(); let row = member_entity::Entity::find() .filter(member_entity::Column::ProjectId.eq(project_id)) .filter(member_entity::Column::UserId.eq(user_id)) .one(&reader) .await?; Ok(row.map(|m| m.role).unwrap_or(ProjectMemberRole::Viewer)) } fn decode_role(v: i32) -> ProjectMemberRole { match v { 0 => ProjectMemberRole::Owner, 1 => ProjectMemberRole::Lead, 2 => ProjectMemberRole::Contributor, _ => ProjectMemberRole::Viewer, } } pub fn role_to_permissions(role: ProjectMemberRole) -> Permissions { match role { ProjectMemberRole::Owner => Permissions::from_vec(&[ VIEW_PROJECT, EDIT_PROJECT, DELETE_PROJECT, TRANSITION_STATE, MANAGE_MEMBERS, MANAGE_DOCUMENTS, MANAGE_VIEWS, MANAGE_LINKS, MANAGE_ACTIVITIES, CREATE_ISSUE_LINK, STAR, ]), ProjectMemberRole::Lead => Permissions::from_vec(&[ VIEW_PROJECT, EDIT_PROJECT, TRANSITION_STATE, MANAGE_MEMBERS, MANAGE_DOCUMENTS, MANAGE_VIEWS, MANAGE_LINKS, MANAGE_ACTIVITIES, CREATE_ISSUE_LINK, STAR, ]), ProjectMemberRole::Contributor => { Permissions::from_vec(&[VIEW_PROJECT, CREATE_ISSUE_LINK, STAR]) } ProjectMemberRole::Viewer => Permissions::from_vec(&[VIEW_PROJECT]), } }