From 3e67a3486eed22522f4352503ef7067ca81a8050 Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Thu, 11 Jul 2024 19:11:51 +0200 Subject: Split server code into multiple smaller modules --- gitolfs3-server/src/authz.rs | 182 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 gitolfs3-server/src/authz.rs (limited to 'gitolfs3-server/src/authz.rs') diff --git a/gitolfs3-server/src/authz.rs b/gitolfs3-server/src/authz.rs new file mode 100644 index 0000000..0674cef --- /dev/null +++ b/gitolfs3-server/src/authz.rs @@ -0,0 +1,182 @@ +use std::collections::HashSet; + +use axum::http::{header, HeaderMap, StatusCode}; +use chrono::{DateTime, Utc}; +use gitolfs3_common::{generate_tag, Claims, Digest, Oid, Operation, SpecificClaims}; + +use crate::{ + api::{make_error_resp, GitLfsErrorResponse, REPO_NOT_FOUND}, + config::AuthorizationConfig, +}; + +pub struct Trusted(pub bool); + +fn forwarded_from_trusted_host( + headers: &HeaderMap, + trusted: &HashSet, +) -> Result> { + if let Some(forwarded_host) = headers.get("X-Forwarded-Host") { + if let Ok(forwarded_host) = forwarded_host.to_str() { + if trusted.contains(forwarded_host) { + return Ok(true); + } + } else { + return Err(make_error_resp( + StatusCode::NOT_FOUND, + "Invalid X-Forwarded-Host header", + )); + } + } + Ok(false) +} + +pub fn authorize_batch( + conf: &AuthorizationConfig, + repo_path: &str, + public: bool, + operation: Operation, + headers: &HeaderMap, +) -> Result> { + // - No authentication required for downloading exported repos + // - When authenticated: + // - Download / upload over presigned URLs + // - When accessing over Tailscale: + // - No authentication required for downloading from any repo + + let claims = VerifyClaimsInput { + specific_claims: SpecificClaims::BatchApi(operation), + repo_path, + }; + if !verify_claims(conf, &claims, headers)? { + return authorize_batch_unauthenticated(conf, public, operation, headers); + } + Ok(Trusted(true)) +} + +fn authorize_batch_unauthenticated( + conf: &AuthorizationConfig, + public: bool, + operation: Operation, + headers: &HeaderMap, +) -> Result> { + let trusted = forwarded_from_trusted_host(headers, &conf.trusted_forwarded_hosts)?; + match operation { + Operation::Upload => { + // Trusted users can clone all repositories (by virtue of accessing the server via a + // trusted network). However, they can not push without proper authentication. Untrusted + // users who are also not authenticated should not need to know which repositories exists. + // Therefore, we tell untrusted && unauthenticated users that the repo doesn't exist, but + // tell trusted users that they need to authenticate. + if !trusted { + return Err(REPO_NOT_FOUND); + } + Err(make_error_resp( + StatusCode::FORBIDDEN, + "Authentication required to upload", + )) + } + Operation::Download => { + // Again, trusted users can see all repos. For untrusted users, we first need to check + // whether the repo is public before we authorize. If the user is untrusted and the + // repo isn't public, we just act like it doesn't even exist. + if !trusted { + if !public { + return Err(REPO_NOT_FOUND); + } + return Ok(Trusted(false)); + } + Ok(Trusted(true)) + } + } +} + +pub fn authorize_get( + conf: &AuthorizationConfig, + repo_path: &str, + oid: Oid, + headers: &HeaderMap, +) -> Result<(), GitLfsErrorResponse<'static>> { + let claims = VerifyClaimsInput { + specific_claims: SpecificClaims::Download(oid), + repo_path, + }; + if !verify_claims(conf, &claims, headers)? { + return Err(make_error_resp( + StatusCode::UNAUTHORIZED, + "Repository not found", + )); + } + Ok(()) +} + +pub struct VerifyClaimsInput<'a> { + pub specific_claims: SpecificClaims, + pub repo_path: &'a str, +} + +fn verify_claims( + conf: &AuthorizationConfig, + claims: &VerifyClaimsInput, + headers: &HeaderMap, +) -> Result> { + const INVALID_AUTHZ_HEADER: GitLfsErrorResponse = + make_error_resp(StatusCode::BAD_REQUEST, "Invalid authorization header"); + + let Some(authz) = headers.get(header::AUTHORIZATION) else { + return Ok(false); + }; + let authz = authz.to_str().map_err(|_| INVALID_AUTHZ_HEADER)?; + let val = authz + .strip_prefix("Gitolfs3-Hmac-Sha256 ") + .ok_or(INVALID_AUTHZ_HEADER)?; + let (tag, expires_at) = val.split_once(' ').ok_or(INVALID_AUTHZ_HEADER)?; + let tag: Digest<32> = tag.parse().map_err(|_| INVALID_AUTHZ_HEADER)?; + let expires_at: i64 = expires_at.parse().map_err(|_| INVALID_AUTHZ_HEADER)?; + let expires_at = DateTime::::from_timestamp(expires_at, 0).ok_or(INVALID_AUTHZ_HEADER)?; + let expected_tag = generate_tag( + Claims { + specific_claims: claims.specific_claims, + repo_path: claims.repo_path, + expires_at, + }, + &conf.key, + ) + .ok_or_else(|| make_error_resp(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"))?; + if tag != expected_tag { + return Err(INVALID_AUTHZ_HEADER); + } + + Ok(true) +} + +#[test] +fn test_validate_claims() { + use gitolfs3_common::Key; + + let key = "00232f7a019bd34e3921ee6c5f04caf48a4489d1be5d1999038950a7054e0bfea369ce2becc0f13fd3c69f8af2384a25b7ac2d52eb52c33722f3c00c50d4c9c2"; + let key: Key = key.parse().unwrap(); + + let claims = Claims { + expires_at: Utc::now() + std::time::Duration::from_secs(5 * 60), + repo_path: "lfs-test.git", + specific_claims: SpecificClaims::BatchApi(Operation::Download), + }; + let tag = generate_tag(claims, &key).unwrap(); + let header_value = format!( + "Gitolfs3-Hmac-Sha256 {tag} {}", + claims.expires_at.timestamp() + ); + + let conf = AuthorizationConfig { + key, + trusted_forwarded_hosts: HashSet::new(), + }; + let verification_claims = VerifyClaimsInput { + repo_path: claims.repo_path, + specific_claims: claims.specific_claims, + }; + let mut headers = HeaderMap::new(); + headers.insert(header::AUTHORIZATION, header_value.try_into().unwrap()); + + assert!(verify_claims(&conf, &verification_claims, &headers).unwrap()); +} -- cgit v1.2.3