alibi_api/plugins/oauth/providers/
gitlab.rs1use super::*;
2impl OAuthProvider {
3 #[must_use]
5 pub fn gitlab(client_id: &str, client_secret: &str) -> Self {
6 Self::gitlab_with_issuer(client_id, client_secret, "https://gitlab.com")
7 }
8
9 pub fn gitlab_with_issuer(client_id: &str, client_secret: &str, issuer: &str) -> Self {
14 let issuer = if issuer.is_empty() {
15 "https://gitlab.com"
16 } else {
17 issuer
18 };
19 Self {
20 client_id: client_id.into(),
21 additional_client_ids: Vec::new(),
22 hosted_domain: None,
23 require_email_verification: false,
24 client_secret: client_secret.into(),
25 auth_url: gitlab_endpoint(issuer, "/oauth/authorize"),
26 token_url: gitlab_endpoint(issuer, "/oauth/token"),
27 user_info_url: Some(gitlab_endpoint(issuer, "/api/v4/user")),
28 scopes: vec!["read_user".into()],
29 authorization: Some(OAuthAuthorizationPolicy::default()),
30 authorization_params: Vec::new(),
31 account_subject: None,
32 map_user_info: Some(gitlab_user_info),
33 get_user_info: None,
34 refresh_access_token: None,
35 verify_id_token: None,
36 id_token: None,
37 disable_id_token_sign_in: false,
38 disable_implicit_sign_up: false,
39 disable_sign_up: false,
40 override_user_info_on_sign_in: false,
41 }
42 }
43}
44
45pub(in crate::plugins::oauth::providers) fn gitlab_endpoint(issuer: &str, suffix: &str) -> String {
46 format!("{issuer}{suffix}")
47 .split("://")
48 .map(|part| {
49 let mut previous_slash = false;
50 part.chars()
51 .filter(|character| {
52 let slash = *character == '/';
53 let retain = !slash || !previous_slash;
54 previous_slash = slash;
55 retain
56 })
57 .collect::<String>()
58 })
59 .collect::<Vec<_>>()
60 .join("://")
61}
62
63#[expect(
64 clippy::needless_pass_by_value,
65 reason = "Match the public provider callback type, which owns its JSON profile"
66)]
67pub(in crate::plugins::oauth::providers) fn gitlab_user_info(
68 profile: Value,
69) -> Result<OAuthUserInfo, String> {
70 let locked = profile.get("locked").is_some_and(|value| match value {
71 Value::Null => false,
72 Value::Bool(value) => *value,
73 Value::Number(value) => value
74 .as_f64()
75 .is_some_and(|value| value != 0.0 && !value.is_nan()),
76 Value::String(value) => !value.is_empty(),
77 Value::Array(_) | Value::Object(_) => true,
78 });
79 if profile.get("state").and_then(Value::as_str) != Some("active") || locked {
80 return Err("GitLab account is inactive or locked".into());
81 }
82 let id = match profile.get("id") {
83 Some(Value::String(value)) => value.clone(),
84 Some(Value::Number(value)) => {
85 alibi_core::utils::json::number_to_string(value).map_err(|error| error.to_string())?
86 }
87 _ => return Err("Missing GitLab account ID".into()),
88 };
89 Ok(OAuthUserInfo {
90 additional_fields: Default::default(),
91 id,
92 email: profile
93 .get("email")
94 .and_then(Value::as_str)
95 .ok_or("Missing GitLab email")?
96 .into(),
97 name: Some(
98 profile
99 .get("name")
100 .filter(|value| !value.is_null())
101 .or_else(|| profile.get("username").filter(|value| !value.is_null()))
102 .and_then(Value::as_str)
103 .unwrap_or("")
104 .into(),
105 ),
106 image: profile
107 .get("avatar_url")
108 .and_then(Value::as_str)
109 .map(String::from),
110 email_verified: profile
111 .get("email_verified")
112 .and_then(Value::as_bool)
113 .unwrap_or(false),
114 })
115}