meritocrab_api/
auth_middleware.rs1use axum::{
2 extract::{Request, State},
3 http::StatusCode,
4 middleware::Next,
5 response::{IntoResponse, Response},
6};
7use meritocrab_github::{CollaboratorRole, GithubApiClient};
8use tower_sessions::Session;
9use tracing::{error, warn};
10
11use crate::error::ApiError;
12use crate::oauth::{GithubUser, get_session_user};
13use std::sync::Arc;
14
15pub async fn require_auth(session: Session, request: Request, next: Next) -> Response {
17 match get_session_user(&session).await {
18 Ok(_user) => {
19 next.run(request).await
21 }
22 Err(e) => {
23 warn!("Unauthorized access attempt: {}", e);
25 (StatusCode::UNAUTHORIZED, "Unauthorized").into_response()
26 }
27 }
28}
29
30pub async fn require_maintainer(
32 State(github_client): State<Arc<GithubApiClient>>,
33 session: Session,
34 mut request: Request,
35 next: Next,
36) -> Response {
37 let user = match get_session_user(&session).await {
39 Ok(user) => user,
40 Err(e) => {
41 warn!("Unauthorized access attempt: {}", e);
42 return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
43 }
44 };
45
46 let path = request.uri().path();
48 let (repo_owner, repo_name) = match extract_repo_from_path(path) {
49 Some(repo) => repo,
50 None => {
51 error!("Failed to extract repo from path: {}", path);
52 return (StatusCode::BAD_REQUEST, "Invalid path").into_response();
53 }
54 };
55
56 match check_user_is_maintainer(&github_client, &user, repo_owner, repo_name).await {
58 Ok(true) => {
59 request.extensions_mut().insert(user);
61 next.run(request).await
62 }
63 Ok(false) => {
64 warn!(
65 "User {} is not a maintainer of {}/{}",
66 user.login, repo_owner, repo_name
67 );
68 (
69 StatusCode::FORBIDDEN,
70 "Forbidden: not a maintainer of this repository",
71 )
72 .into_response()
73 }
74 Err(e) => {
75 error!("Error checking maintainer status: {}", e);
76 (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response()
77 }
78 }
79}
80
81fn extract_repo_from_path(path: &str) -> Option<(&str, &str)> {
84 let parts: Vec<&str> = path.split('/').collect();
85
86 if parts.len() < 5 || parts[1] != "api" || parts[2] != "repos" {
88 return None;
89 }
90
91 Some((parts[3], parts[4]))
92}
93
94async fn check_user_is_maintainer(
96 github_client: &GithubApiClient,
97 user: &GithubUser,
98 repo_owner: &str,
99 repo_name: &str,
100) -> Result<bool, ApiError> {
101 match github_client
103 .check_collaborator_role(repo_owner, repo_name, &user.login)
104 .await
105 {
106 Ok(role) => {
107 Ok(matches!(
109 role,
110 CollaboratorRole::Admin | CollaboratorRole::Maintain | CollaboratorRole::Write
111 ))
112 }
113 Err(e) => {
114 error!(
115 "Failed to check role for user {} in {}/{}: {}",
116 user.login, repo_owner, repo_name, e
117 );
118 Ok(false)
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_extract_repo_from_path() {
130 assert_eq!(
131 extract_repo_from_path("/api/repos/owner/repo/evaluations"),
132 Some(("owner", "repo"))
133 );
134
135 assert_eq!(
136 extract_repo_from_path("/api/repos/my-org/my-repo/contributors"),
137 Some(("my-org", "my-repo"))
138 );
139
140 assert_eq!(extract_repo_from_path("/api/repos/owner"), None);
141
142 assert_eq!(extract_repo_from_path("/webhooks/github"), None);
143
144 assert_eq!(extract_repo_from_path("/health"), None);
145 }
146}