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