1use super::GitLab;
2use flams_ontology::uris::ArchiveId;
3use flams_utils::unwrap;
4use gitlab::api::AsyncQuery;
5pub use oauth2::AccessToken;
6use oauth2::{url::Url, TokenResponse};
7use tracing::{instrument, Instrument};
8
9#[derive(Debug, Clone)]
10pub struct GitLabOAuth(
11 oauth2::basic::BasicClient<
12 oauth2::EndpointSet,
13 oauth2::EndpointNotSet,
14 oauth2::EndpointNotSet,
15 oauth2::EndpointNotSet,
16 oauth2::EndpointSet,
17 >,
18 GitLab,
19);
20
21#[derive(Debug, Clone, serde::Deserialize)]
22pub struct AuthRequest {
23 pub code: String,
24 }
26
27impl GitLabOAuth {
28 pub fn login_url(&self) -> Url {
29 let (url, _) = self
30 .0
31 .authorize_url(oauth2::CsrfToken::new_random)
32 .add_scope(oauth2::Scope::new("read_user".to_string()))
33 .add_scope(oauth2::Scope::new("api".to_string()))
34 .url();
35 url
36 }
37 pub async fn callback(
39 &self,
40 request: AuthRequest,
41 ) -> Result<AccessToken, impl std::error::Error> {
42 use oauth2::AuthorizationCode;
43 let code = AuthorizationCode::new(request.code);
44 let req = unwrap!(oauth2::reqwest::ClientBuilder::new().build().ok());
45 self.0
46 .exchange_code(code)
47 .request_async(&req)
48 .instrument(crate::REMOTE_SPAN.clone())
49 .await
50 .map(|token| token.access_token().clone())
51 }
52
53 pub async fn get_projects(&self, token: String) -> Result<Vec<crate::Project>, super::Err> {
55 self.get_projects_i(token)
56 .instrument(crate::REMOTE_SPAN.clone())
57 .await
58 }
59 #[instrument(
60 level = "debug",
61 target = "git",
62 name = "getting all projects for user",
63 skip_all
64 )]
65 async fn get_projects_i(&self, token: String) -> Result<Vec<crate::Project>, super::Err> {
66 let mut client = gitlab::ImpersonationClient::new(&self.1 .0.inner, token);
67 client.oauth2_token();
68 let r = gitlab::api::projects::Projects::builder()
69 .simple(true)
70 .min_access_level(gitlab::api::common::AccessLevel::Developer)
71 .build()
73 .unwrap_or_else(|_| unreachable!());
74 let r: Vec<crate::Project> = gitlab::api::paged(r, gitlab::api::Pagination::All)
75 .query_async(&client)
76 .await?;
77 let mut vs = self.1 .0.projects.lock();
78 for p in &r {
79 if !vs.contains(&p.id) {
80 vs.insert(super::ProjectWithId {
81 project: p.clone(),
82 id: None,
83 });
84 }
85 }
86 drop(vs);
87 Ok(r)
88 }
89
90 pub async fn get_archive_id(
92 &self,
93 id: u64,
94 token: String,
95 branch: &str,
96 ) -> Result<Option<ArchiveId>, super::Err> {
97 self.get_archive_id_i(id, token, branch)
98 .instrument(crate::REMOTE_SPAN.clone())
99 .await
100 }
101 #[instrument(
102 level = "debug",
103 target = "git",
104 name = "getting archive id",
105 skip(self, token)
106 )]
107 pub async fn get_archive_id_i(
108 &self,
109 id: u64,
110 token: String,
111 branch: &str,
112 ) -> Result<Option<ArchiveId>, super::Err> {
113 {
114 let vs = self.1 .0.projects.lock();
115 if let Some(super::ProjectWithId { id: Some(id), .. }) = vs.get(&id) {
116 return Ok(id.clone());
117 }
118 }
119
120 macro_rules! ret {
121 ($v:expr) => {{
122 tracing::info!("Found {:?}", $v);
123 let mut lock = self.1 .0.projects.lock();
124 if let Some(mut v) = lock.take(&id) {
125 v.id = Some($v.clone());
126 lock.insert(v);
127 }
128 drop(lock);
129 return Ok($v);
130 }};
131 }
132 let mut client = gitlab::ImpersonationClient::new(&self.1 .0.inner, token);
133 client.oauth2_token();
134 let r = gitlab::api::projects::repository::TreeBuilder::default()
135 .project(id)
136 .ref_(branch)
137 .recursive(false)
138 .build()
139 .unwrap_or_else(|_| unreachable!());
140 let r: Vec<crate::TreeEntry> = r.query_async(&client).await?;
141 let Some(p) = r.into_iter().find_map(|e| {
142 if e.path.eq_ignore_ascii_case("meta-inf") && matches!(e.kind, crate::DirOrFile::Dir) {
143 Some(e.path)
144 } else {
145 None
146 }
147 }) else {
148 ret!(None::<ArchiveId>)
149 };
150
151 let r = gitlab::api::projects::repository::TreeBuilder::default()
152 .project(id)
153 .ref_(branch)
154 .path(p)
155 .recursive(false)
156 .build()
157 .unwrap_or_else(|_| unreachable!());
158 let r: Vec<crate::TreeEntry> = r.query_async(&client).await?;
159 let Some(p) = r.into_iter().find_map(|e| {
160 if e.name.eq_ignore_ascii_case("manifest.mf")
161 && matches!(e.kind, crate::DirOrFile::File)
162 {
163 Some(e.path)
164 } else {
165 None
166 }
167 }) else {
168 ret!(None::<ArchiveId>)
169 };
170
171 let blob = gitlab::api::projects::repository::files::FileRaw::builder()
172 .project(id)
173 .file_path(p)
174 .ref_(branch)
175 .build()
176 .unwrap_or_else(|_| unreachable!());
177 let r = gitlab::api::raw(blob).query_async(&client).await?;
178 let r = std::str::from_utf8(&r)?;
179 let r = r.split('\n').find_map(|line| {
180 let line = line.trim();
181 line.strip_prefix("id:")
182 .map(|rest| ArchiveId::new(rest.trim()))
183 });
184 ret!(r)
185 }
186
187 pub async fn get_branches(
189 &self,
190 id: u64,
191 token: String,
192 ) -> Result<Vec<crate::Branch>, super::Err> {
193 self.get_branches_i(id, token)
194 .instrument(crate::REMOTE_SPAN.clone())
195 .await
196 }
197 #[instrument(
198 level = "debug",
199 target = "git",
200 name = "getting branches for archive",
201 skip(self, token)
202 )]
203 pub async fn get_branches_i(
204 &self,
205 id: u64,
206 token: String,
207 ) -> Result<Vec<crate::Branch>, super::Err> {
208 let mut client = gitlab::ImpersonationClient::new(&self.1 .0.inner, token);
209 client.oauth2_token();
210 let r = gitlab::api::projects::repository::branches::Branches::builder()
211 .project(id)
212 .build()
213 .unwrap_or_else(|_| unreachable!())
214 .query_async(&client)
215 .await?;
216 Ok(r)
217 }
218}
219
220impl GitLab {
221 pub fn new_oauth(&self, redirect: &str) -> Result<GitLabOAuth, OAuthError> {
223 use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
224 let url = &self.0.url;
225 let Some(app_id) = &self.0.id else {
226 return Err(OAuthError::MissingAppID);
227 };
228 let Some(app_secret) = &self.0.secret else {
229 return Err(OAuthError::MissingAppSecret);
230 };
231 let Ok(auth_url) = AuthUrl::new(format!("{url}/oauth/authorize")) else {
232 return Err(OAuthError::InvalidAuthURL(format!("{url}/oauth/authorize")));
233 };
234 let Ok(token_url) = TokenUrl::new(format!("{url}/oauth/token")) else {
235 return Err(OAuthError::InvalidTokenURL(format!("{url}/oauth/token")));
236 };
237 let Ok(redirect_url) = RedirectUrl::new(redirect.to_string()) else {
238 return Err(OAuthError::InvalidRedirectURL(redirect.to_string()));
239 };
240 let auth = oauth2::basic::BasicClient::new(ClientId::new(app_id.to_string()))
241 .set_client_secret(ClientSecret::new(app_secret.to_string()))
242 .set_auth_uri(auth_url)
243 .set_token_uri(token_url)
244 .set_redirect_uri(redirect_url);
245 Ok(GitLabOAuth(auth, self.clone()))
246 }
247
248 pub async fn get_oauth_user(
250 &self,
251 token: &oauth2::AccessToken,
252 ) -> Result<GitlabUser, reqwest::Error> {
253 let client = reqwest::Client::new();
254 let resp = client
255 .get(format!("{}/api/v4/user", self.0.url))
256 .bearer_auth(token.secret())
257 .send()
258 .instrument(crate::REMOTE_SPAN.clone())
259 .await?;
260 resp.json().instrument(crate::REMOTE_SPAN.clone()).await
261 }
262}
263
264#[derive(Debug)]
265pub enum OAuthError {
266 InvalidAuthURL(String),
267 InvalidTokenURL(String),
268 InvalidRedirectURL(String),
269 MissingAppID,
270 MissingAppSecret,
271}
272impl std::fmt::Display for OAuthError {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 match self {
275 Self::InvalidAuthURL(s) => write!(f, "Invalid auth url: {s}"),
276 Self::InvalidTokenURL(s) => write!(f, "Invalid token url: {s}"),
277 Self::InvalidRedirectURL(s) => write!(f, "Invalid redirect url: {s}"),
278 Self::MissingAppID => write!(f, "Missing app id"),
279 Self::MissingAppSecret => write!(f, "Missing secret"),
280 }
281 }
282}
283
284impl std::error::Error for OAuthError {}
285
286#[derive(Debug, serde::Deserialize, serde::Serialize)]
287pub struct GitlabUser {
288 pub id: i64,
289 pub username: String,
290 pub name: String,
291 pub avatar_url: String,
293 pub email: Option<String>,
295 pub can_create_group: bool,
297 pub can_create_project: bool,
298}
299
300