1mod backoff;
2mod cache;
3mod credentials;
4mod gitea;
5mod github;
6mod gitlab;
7
8use std::collections::HashMap;
9
10use axum::extract::{Path, Request, State};
11use axum::http::HeaderMap;
12use axum::middleware::Next;
13use axum::response::Response;
14
15use crate::config::{Auth, Provider};
16use crate::error::Error;
17use crate::namespace::Namespace;
18use crate::state::Shared;
19use cache::{Cache, Caller, Decision, IdentityCache};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Permission {
23 Read,
24 Write,
25 Admin,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Actor(pub String);
30
31impl Permission {
32 pub fn require_write(self) -> Result<(), Error> {
33 matches!(self, Self::Write | Self::Admin)
34 .then_some(())
35 .ok_or(Error::Forbidden)
36 }
37
38 pub fn require_admin(self) -> Result<(), Error> {
39 matches!(self, Self::Admin)
40 .then_some(())
41 .ok_or(Error::Forbidden)
42 }
43}
44
45pub enum Authorizer {
46 Forge {
47 provider: Provider,
48 client: reqwest::Client,
49 api_url: String,
50 cache: Cache,
51 identities: IdentityCache,
52 anonymous_read: bool,
53 },
54 Disabled,
55}
56
57impl Authorizer {
58 pub fn new(auth: &Auth) -> Self {
59 crate::tls::install_crypto_provider();
60
61 match auth {
62 Auth::Disabled => Self::Disabled,
63 Auth::Forge {
64 provider,
65 api_url,
66 cache_ttl,
67 rejection_ttl,
68 anonymous_read,
69 } => Self::Forge {
70 provider: *provider,
71 client: reqwest::Client::builder()
72 .user_agent(concat!("lfsx/", env!("CARGO_PKG_VERSION")))
73 .timeout(std::time::Duration::from_secs(10))
74 .build()
75 .expect("http client"),
76 api_url: api_url.clone(),
77 cache: Cache::new(*cache_ttl, *rejection_ttl),
78 identities: IdentityCache::new(*cache_ttl),
79 anonymous_read: *anonymous_read,
80 },
81 }
82 }
83
84 async fn permission(&self, headers: &HeaderMap, ns: &Namespace) -> Result<Permission, Error> {
85 let Self::Forge {
86 provider,
87 client,
88 api_url,
89 cache,
90 anonymous_read,
91 ..
92 } = self
93 else {
94 return Ok(Permission::Admin);
95 };
96
97 let Some(token) = credentials::token(headers) else {
102 if !*anonymous_read {
103 return Err(Error::Unauthenticated);
104 }
105
106 if let Some(decision) = cache.get(Caller::Anonymous, ns) {
107 return decision.into();
108 }
109
110 let outcome = match provider {
111 Provider::Github => github::public(client, api_url, ns).await,
112 Provider::Gitlab => gitlab::public(client, api_url, ns).await,
113 Provider::Gitea => gitea::public(client, api_url, ns).await,
114 };
115 if let Some(decision) = Decision::of(&outcome) {
116 cache.insert(Caller::Anonymous, ns, decision);
117 }
118
119 return outcome;
120 };
121
122 if let Some(decision) = cache.get(Caller::Token(&token), ns) {
123 return decision.into();
124 }
125
126 let outcome = match provider {
127 Provider::Github => github::permission(client, api_url, &token, ns).await,
128 Provider::Gitlab => gitlab::permission(client, api_url, &token, ns).await,
129 Provider::Gitea => gitea::permission(client, api_url, &token, ns).await,
130 };
131 if let Some(decision) = Decision::of(&outcome) {
132 cache.insert(Caller::Token(&token), ns, decision);
133 }
134
135 outcome
136 }
137}
138
139impl Authorizer {
140 pub async fn actor(&self, headers: &HeaderMap) -> Result<Actor, Error> {
141 let Self::Forge {
142 provider,
143 client,
144 api_url,
145 identities,
146 ..
147 } = self
148 else {
149 return Ok(Actor("anonymous".to_owned()));
150 };
151
152 let token = credentials::token(headers).ok_or(Error::Unauthenticated)?;
153 if let Some(login) = identities.get(&token) {
154 return Ok(Actor(login));
155 }
156
157 let login = match provider {
158 Provider::Github => github::login(client, api_url, &token).await?,
159 Provider::Gitlab => gitlab::login(client, api_url, &token).await?,
160 Provider::Gitea => gitea::login(client, api_url, &token).await?,
161 };
162 identities.insert(&token, &login);
163
164 Ok(Actor(login))
165 }
166}
167
168pub async fn authorize(
169 State(state): State<Shared>,
170 Path(params): Path<HashMap<String, String>>,
171 mut request: Request,
172 next: Next,
173) -> Result<Response, Error> {
174 let (Some(org), Some(repo)) = (params.get("org"), params.get("repo")) else {
175 return Err(Error::MalformedNamespace);
176 };
177 let ns = Namespace::new(org.as_str(), repo.as_str())?;
178
179 let permission = state.authorizer.permission(request.headers(), &ns).await?;
180 request.extensions_mut().insert(permission);
181 request.extensions_mut().insert(ns);
182
183 Ok(next.run(request).await)
184}
185
186#[cfg(test)]
187mod tests;