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