d5939db
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<Permissions> {
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<Permissions> {
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<Permissions> {
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<ProjectMemberRole> {
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]),
}
}