Skip to main content

meritocrab_api/
auth_middleware.rs

1use 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
15/// Auth middleware that checks if user is authenticated
16pub async fn require_auth(session: Session, request: Request, next: Next) -> Response {
17    match get_session_user(&session).await {
18        Ok(_user) => {
19            // User is authenticated, proceed
20            next.run(request).await
21        }
22        Err(e) => {
23            // User is not authenticated
24            warn!("Unauthorized access attempt: {}", e);
25            (StatusCode::UNAUTHORIZED, "Unauthorized").into_response()
26        }
27    }
28}
29
30/// Auth middleware that checks if user is a maintainer of the repo
31pub async fn require_maintainer(
32    State(github_client): State<Arc<GithubApiClient>>,
33    session: Session,
34    mut request: Request,
35    next: Next,
36) -> Response {
37    // First check if user is authenticated
38    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    // Extract repo owner and name from path
47    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    // Check if user is a maintainer of the repo
57    match check_user_is_maintainer(&github_client, &user, repo_owner, repo_name).await {
58        Ok(true) => {
59            // User is a maintainer, store user in request extensions
60            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
81/// Extract repo owner and name from API path
82/// Expects paths like /api/repos/{owner}/{repo}/...
83fn extract_repo_from_path(path: &str) -> Option<(&str, &str)> {
84    let parts: Vec<&str> = path.split('/').collect();
85
86    // Expected pattern: ["", "api", "repos", "{owner}", "{repo}", ...]
87    if parts.len() < 5 || parts[1] != "api" || parts[2] != "repos" {
88        return None;
89    }
90
91    Some((parts[3], parts[4]))
92}
93
94/// Check if user is a maintainer of the repository
95async 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    // Use GitHub API to check user's role
102    match github_client
103        .check_collaborator_role(repo_owner, repo_name, &user.login)
104        .await
105    {
106        Ok(role) => {
107            // Maintainers, admins, and write access have permission
108            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            // If we can't check the role, deny access
119            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}