aboutsummaryrefslogtreecommitdiffstats
path: root/gitolfs3-server/src/authz.rs
diff options
context:
space:
mode:
authorLibravatar Rutger Broekhoff2024-07-11 19:11:51 +0200
committerLibravatar Rutger Broekhoff2024-07-11 19:11:51 +0200
commit3e67a3486eed22522f4352503ef7067ca81a8050 (patch)
tree971f0a84c073731c944069bdefdb848099428bdf /gitolfs3-server/src/authz.rs
parent0822bdf658e03bdb5b31a90b8c64f3d0b6a6cff7 (diff)
downloadgitolfs3-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.rs182
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 @@
1use std::collections::HashSet;
2
3use axum::http::{header, HeaderMap, StatusCode};
4use chrono::{DateTime, Utc};
5use gitolfs3_common::{generate_tag, Claims, Digest, Oid, Operation, SpecificClaims};
6
7use crate::{
8 api::{make_error_resp, GitLfsErrorResponse, REPO_NOT_FOUND},
9 config::AuthorizationConfig,
10};
11
12pub struct Trusted(pub bool);
13
14fn 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
33pub 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
56fn 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
93pub 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
112pub struct VerifyClaimsInput<'a> {
113 pub specific_claims: SpecificClaims,
114 pub repo_path: &'a str,
115}
116
117fn 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]
153fn 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}