jj workspaces over the network
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(server): add authorization - role-based access control

+110
+110
crates/tandem-server/src/authz.rs
··· 1 + use axum::http::StatusCode; 2 + use crate::{AppState, auth::AuthenticatedUser}; 3 + 4 + /// Role levels (ordered by permission level) 5 + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 6 + pub enum Role { 7 + Read = 0, 8 + Write = 1, 9 + Admin = 2, 10 + } 11 + 12 + impl Role { 13 + pub fn from_str(s: &str) -> Option<Self> { 14 + match s { 15 + "read" => Some(Role::Read), 16 + "write" => Some(Role::Write), 17 + "admin" => Some(Role::Admin), 18 + _ => None, 19 + } 20 + } 21 + 22 + pub fn as_str(&self) -> &'static str { 23 + match self { 24 + Role::Read => "read", 25 + Role::Write => "write", 26 + Role::Admin => "admin", 27 + } 28 + } 29 + } 30 + 31 + /// Check if user has at least the required role for a repo 32 + pub async fn check_role( 33 + state: &AppState, 34 + user: &AuthenticatedUser, 35 + repo_id: &str, 36 + required: Role, 37 + ) -> Result<(), StatusCode> { 38 + let role_str = state.db.get_user_role(&user.id, repo_id).await 39 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 40 + 41 + let role = role_str 42 + .and_then(|s| Role::from_str(&s)) 43 + .ok_or(StatusCode::FORBIDDEN)?; 44 + 45 + if role >= required { 46 + Ok(()) 47 + } else { 48 + Err(StatusCode::FORBIDDEN) 49 + } 50 + } 51 + 52 + /// Helper to require read access 53 + pub async fn require_read( 54 + state: &AppState, 55 + user: &AuthenticatedUser, 56 + repo_id: &str, 57 + ) -> Result<(), StatusCode> { 58 + check_role(state, user, repo_id, Role::Read).await 59 + } 60 + 61 + /// Helper to require write access 62 + pub async fn require_write( 63 + state: &AppState, 64 + user: &AuthenticatedUser, 65 + repo_id: &str, 66 + ) -> Result<(), StatusCode> { 67 + check_role(state, user, repo_id, Role::Write).await 68 + } 69 + 70 + /// Helper to require admin access 71 + pub async fn require_admin( 72 + state: &AppState, 73 + user: &AuthenticatedUser, 74 + repo_id: &str, 75 + ) -> Result<(), StatusCode> { 76 + check_role(state, user, repo_id, Role::Admin).await 77 + } 78 + 79 + /// Check if a bookmark is protected 80 + pub async fn is_bookmark_protected( 81 + state: &AppState, 82 + repo_id: &str, 83 + bookmark_name: &str, 84 + ) -> Result<bool, StatusCode> { 85 + let doc = state.docs.get_or_load(repo_id).await 86 + .map_err(|_| StatusCode::NOT_FOUND)?; 87 + 88 + let doc = doc.read().await; 89 + let _bookmarks = doc.get_all_bookmarks(); 90 + 91 + // For now, consider bookmarks named "main" or "master" as protected 92 + // TODO: Make this configurable per-repo 93 + Ok(matches!(bookmark_name, "main" | "master")) 94 + } 95 + 96 + /// Check if user can move a bookmark 97 + pub async fn can_move_bookmark( 98 + state: &AppState, 99 + user: &AuthenticatedUser, 100 + repo_id: &str, 101 + bookmark_name: &str, 102 + ) -> Result<(), StatusCode> { 103 + let protected = is_bookmark_protected(state, repo_id, bookmark_name).await?; 104 + 105 + if protected { 106 + require_admin(state, user, repo_id).await 107 + } else { 108 + require_write(state, user, repo_id).await 109 + } 110 + }