diff options
| author | Rutger Broekhoff | 2024-07-11 19:11:51 +0200 |
|---|---|---|
| committer | Rutger Broekhoff | 2024-07-11 19:11:51 +0200 |
| commit | 3e67a3486eed22522f4352503ef7067ca81a8050 (patch) | |
| tree | 971f0a84c073731c944069bdefdb848099428bdf /gitolfs3-server/src/authz.rs | |
| parent | 0822bdf658e03bdb5b31a90b8c64f3d0b6a6cff7 (diff) | |
| download | gitolfs3-3e67a3486eed22522f4352503ef7067ca81a8050.tar.gz gitolfs3-3e67a3486eed22522f4352503ef7067ca81a8050.zip | |
Split server code into multiple smaller modules
Diffstat (limited to 'gitolfs3-server/src/authz.rs')
| -rw-r--r-- | gitolfs3-server/src/authz.rs | 182 |
1 files changed, 182 insertions, 0 deletions
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 @@ | |||
| 1 | use std::collections::HashSet; | ||
| 2 | |||
| 3 | use axum::http::{header, HeaderMap, StatusCode}; | ||
| 4 | use chrono::{DateTime, Utc}; | ||
| 5 | use gitolfs3_common::{generate_tag, Claims, Digest, Oid, Operation, SpecificClaims}; | ||
| 6 | |||
| 7 | use crate::{ | ||
| 8 | api::{make_error_resp, GitLfsErrorResponse, REPO_NOT_FOUND}, | ||
| 9 | config::AuthorizationConfig, | ||
| 10 | }; | ||
| 11 | |||
| 12 | pub struct Trusted(pub bool); | ||
| 13 | |||
| 14 | fn forwarded_from_trusted_host( | ||
| 15 | headers: &HeaderMap, | ||
| 16 | trusted: &HashSet<String>, | ||
| 17 | ) -> Result<bool, GitLfsErrorResponse<'static>> { | ||
| 18 | if let Some(forwarded_host) = headers.get("X-Forwarded-Host") { | ||
| 19 | if let Ok(forwarded_host) = forwarded_host.to_str() { | ||
| 20 | if trusted.contains(forwarded_host) { | ||
| 21 | return Ok(true); | ||
| 22 | } | ||
| 23 | } else { | ||
| 24 | return Err(make_error_resp( | ||
| 25 | StatusCode::NOT_FOUND, | ||
| 26 | "Invalid X-Forwarded-Host header", | ||
| 27 | )); | ||
| 28 | } | ||
| 29 | } | ||
| 30 | Ok(false) | ||
| 31 | } | ||
| 32 | |||
| 33 | pub fn authorize_batch( | ||
| 34 | conf: &AuthorizationConfig, | ||
| 35 | repo_path: &str, | ||
| 36 | public: bool, | ||
| 37 | operation: Operation, | ||
| 38 | headers: &HeaderMap, | ||
| 39 | ) -> Result<Trusted, GitLfsErrorResponse<'static>> { | ||
| 40 | // - No authentication required for downloading exported repos | ||
| 41 | // - When authenticated: | ||
| 42 | // - Download / upload over presigned URLs | ||
| 43 | // - When accessing over Tailscale: | ||
| 44 | // - No authentication required for downloading from any repo | ||
| 45 | |||
| 46 | let claims = VerifyClaimsInput { | ||
| 47 | specific_claims: SpecificClaims::BatchApi(operation), | ||
| 48 | repo_path, | ||
| 49 | }; | ||
| 50 | if !verify_claims(conf, &claims, headers)? { | ||
| 51 | return authorize_batch_unauthenticated(conf, public, operation, headers); | ||
| 52 | } | ||
| 53 | Ok(Trusted(true)) | ||
| 54 | } | ||
| 55 | |||
| 56 | fn authorize_batch_unauthenticated( | ||
| 57 | conf: &AuthorizationConfig, | ||
| 58 | public: bool, | ||
| 59 | operation: Operation, | ||
| 60 | headers: &HeaderMap, | ||
| 61 | ) -> Result<Trusted, GitLfsErrorResponse<'static>> { | ||
| 62 | let trusted = forwarded_from_trusted_host(headers, &conf.trusted_forwarded_hosts)?; | ||
| 63 | match operation { | ||
| 64 | Operation::Upload => { | ||
| 65 | // Trusted users can clone all repositories (by virtue of accessing the server via a | ||
| 66 | // trusted network). However, they can not push without proper authentication. Untrusted | ||
| 67 | // users who are also not authenticated should not need to know which repositories exists. | ||
| 68 | // Therefore, we tell untrusted && unauthenticated users that the repo doesn't exist, but | ||
| 69 | // tell trusted users that they need to authenticate. | ||
| 70 | if !trusted { | ||
| 71 | return Err(REPO_NOT_FOUND); | ||
| 72 | } | ||
| 73 | Err(make_error_resp( | ||
| 74 | StatusCode::FORBIDDEN, | ||
| 75 | "Authentication required to upload", | ||
| 76 | )) | ||
| 77 | } | ||
| 78 | Operation::Download => { | ||
| 79 | // Again, trusted users can see all repos. For untrusted users, we first need to check | ||
| 80 | // whether the repo is public before we authorize. If the user is untrusted and the | ||
| 81 | // repo isn't public, we just act like it doesn't even exist. | ||
| 82 | if !trusted { | ||
| 83 | if !public { | ||
| 84 | return Err(REPO_NOT_FOUND); | ||
| 85 | } | ||
| 86 | return Ok(Trusted(false)); | ||
| 87 | } | ||
| 88 | Ok(Trusted(true)) | ||
| 89 | } | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | pub fn authorize_get( | ||
| 94 | conf: &AuthorizationConfig, | ||
| 95 | repo_path: &str, | ||
| 96 | oid: Oid, | ||
| 97 | headers: &HeaderMap, | ||
| 98 | ) -> Result<(), GitLfsErrorResponse<'static>> { | ||
| 99 | let claims = VerifyClaimsInput { | ||
| 100 | specific_claims: SpecificClaims::Download(oid), | ||
| 101 | repo_path, | ||
| 102 | }; | ||
| 103 | if !verify_claims(conf, &claims, headers)? { | ||
| 104 | return Err(make_error_resp( | ||
| 105 | StatusCode::UNAUTHORIZED, | ||
| 106 | "Repository not found", | ||
| 107 | )); | ||
| 108 | } | ||
| 109 | Ok(()) | ||
| 110 | } | ||
| 111 | |||
| 112 | pub struct VerifyClaimsInput<'a> { | ||
| 113 | pub specific_claims: SpecificClaims, | ||
| 114 | pub repo_path: &'a str, | ||
| 115 | } | ||
| 116 | |||
| 117 | fn verify_claims( | ||
| 118 | conf: &AuthorizationConfig, | ||
| 119 | claims: &VerifyClaimsInput, | ||
| 120 | headers: &HeaderMap, | ||
| 121 | ) -> Result<bool, GitLfsErrorResponse<'static>> { | ||
| 122 | const INVALID_AUTHZ_HEADER: GitLfsErrorResponse = | ||
| 123 | make_error_resp(StatusCode::BAD_REQUEST, "Invalid authorization header"); | ||
| 124 | |||
| 125 | let Some(authz) = headers.get(header::AUTHORIZATION) else { | ||
| 126 | return Ok(false); | ||
| 127 | }; | ||
| 128 | let authz = authz.to_str().map_err(|_| INVALID_AUTHZ_HEADER)?; | ||
| 129 | let val = authz | ||
| 130 | .strip_prefix("Gitolfs3-Hmac-Sha256 ") | ||
| 131 | .ok_or(INVALID_AUTHZ_HEADER)?; | ||
| 132 | let (tag, expires_at) = val.split_once(' ').ok_or(INVALID_AUTHZ_HEADER)?; | ||
| 133 | let tag: Digest<32> = tag.parse().map_err(|_| INVALID_AUTHZ_HEADER)?; | ||
| 134 | let expires_at: i64 = expires_at.parse().map_err(|_| INVALID_AUTHZ_HEADER)?; | ||
| 135 | let expires_at = DateTime::<Utc>::from_timestamp(expires_at, 0).ok_or(INVALID_AUTHZ_HEADER)?; | ||
| 136 | let expected_tag = generate_tag( | ||
| 137 | Claims { | ||
| 138 | specific_claims: claims.specific_claims, | ||
| 139 | repo_path: claims.repo_path, | ||
| 140 | expires_at, | ||
| 141 | }, | ||
| 142 | &conf.key, | ||
| 143 | ) | ||
| 144 | .ok_or_else(|| make_error_resp(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"))?; | ||
| 145 | if tag != expected_tag { | ||
| 146 | return Err(INVALID_AUTHZ_HEADER); | ||
| 147 | } | ||
| 148 | |||
| 149 | Ok(true) | ||
| 150 | } | ||
| 151 | |||
| 152 | #[test] | ||
| 153 | fn test_validate_claims() { | ||
| 154 | use gitolfs3_common::Key; | ||
| 155 | |||
| 156 | let key = "00232f7a019bd34e3921ee6c5f04caf48a4489d1be5d1999038950a7054e0bfea369ce2becc0f13fd3c69f8af2384a25b7ac2d52eb52c33722f3c00c50d4c9c2"; | ||
| 157 | let key: Key = key.parse().unwrap(); | ||
| 158 | |||
| 159 | let claims = Claims { | ||
| 160 | expires_at: Utc::now() + std::time::Duration::from_secs(5 * 60), | ||
| 161 | repo_path: "lfs-test.git", | ||
| 162 | specific_claims: SpecificClaims::BatchApi(Operation::Download), | ||
| 163 | }; | ||
| 164 | let tag = generate_tag(claims, &key).unwrap(); | ||
| 165 | let header_value = format!( | ||
| 166 | "Gitolfs3-Hmac-Sha256 {tag} {}", | ||
| 167 | claims.expires_at.timestamp() | ||
| 168 | ); | ||
| 169 | |||
| 170 | let conf = AuthorizationConfig { | ||
| 171 | key, | ||
| 172 | trusted_forwarded_hosts: HashSet::new(), | ||
| 173 | }; | ||
| 174 | let verification_claims = VerifyClaimsInput { | ||
| 175 | repo_path: claims.repo_path, | ||
| 176 | specific_claims: claims.specific_claims, | ||
| 177 | }; | ||
| 178 | let mut headers = HeaderMap::new(); | ||
| 179 | headers.insert(header::AUTHORIZATION, header_value.try_into().unwrap()); | ||
| 180 | |||
| 181 | assert!(verify_claims(&conf, &verification_claims, &headers).unwrap()); | ||
| 182 | } | ||