gitlab/api/users/personal_access_tokens/
create_for_user.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use std::collections::BTreeSet;
8
9use chrono::NaiveDate;
10use derive_builder::Builder;
11
12use crate::api::endpoint_prelude::*;
13use crate::api::ParamValue;
14
15/// Scopes for personal access tokens.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17#[non_exhaustive]
18pub enum PersonalAccessTokenScope {
19    /// Access the API and perform git reads and writes.
20    Api,
21    /// Access to read the user information.
22    ReadUser,
23    /// Access read-only API endpoints.
24    ReadApi,
25    /// Read access to repositories.
26    ReadRepository,
27    /// Write access to repositories.
28    WriteRepository,
29    /// Read access to Docker registries.
30    ReadRegistry,
31    /// Write access to Docker registries.
32    WriteRegistry,
33    /// Permission to `sudo` as other users (administrator only).
34    Sudo,
35    /// Permission to access administrator API actions.
36    AdminMode,
37    /// Permission to create instance runners.
38    CreateRunner,
39    /// Access to AI features (GitLab Duo for JetBrains).
40    AiFeatures,
41    /// Access to perform Kubernetes API calls.
42    K8sProxy,
43    /// Access to the Service Ping payload.
44    ReadServicePing,
45}
46
47impl PersonalAccessTokenScope {
48    /// The scope as a query parameter.
49    pub(crate) fn as_str(self) -> &'static str {
50        match self {
51            Self::Api => "api",
52            Self::ReadUser => "read_user",
53            Self::ReadApi => "read_api",
54            Self::ReadRepository => "read_repository",
55            Self::WriteRepository => "write_repository",
56            Self::ReadRegistry => "read_registry",
57            Self::WriteRegistry => "write_registry",
58            Self::Sudo => "sudo",
59            Self::AdminMode => "admin_mode",
60            Self::CreateRunner => "create_runner",
61            Self::AiFeatures => "ai_features",
62            Self::K8sProxy => "k8s_proxy",
63            Self::ReadServicePing => "read_service_ping",
64        }
65    }
66
67    /// Transform into a `create` scope if possible.
68    pub fn as_create_scope(self) -> Option<super::PersonalAccessTokenCreateScope> {
69        match self {
70            Self::K8sProxy => Some(super::PersonalAccessTokenCreateScope::K8sProxy),
71            _ => None,
72        }
73    }
74}
75
76impl ParamValue<'static> for PersonalAccessTokenScope {
77    fn as_value(&self) -> Cow<'static, str> {
78        self.as_str().into()
79    }
80}
81
82/// Create a new personal access token for a user.
83#[derive(Debug, Builder, Clone)]
84#[builder(setter(strip_option))]
85pub struct CreatePersonalAccessTokenForUser<'a> {
86    /// The user to create a personal access token for.
87    user: u64,
88    /// The name of the personal access token.
89    #[builder(setter(into))]
90    name: Cow<'a, str>,
91    /// The scopes to allow the token to access.
92    #[builder(setter(name = "_scopes"), private)]
93    scopes: BTreeSet<PersonalAccessTokenScope>,
94
95    /// An optional description for the personal access token.
96    #[builder(setter(into), default)]
97    description: Option<Cow<'a, str>>,
98    /// When the token expires.
99    #[builder(default)]
100    expires_at: Option<NaiveDate>,
101}
102
103impl<'a> CreatePersonalAccessTokenForUser<'a> {
104    /// Create a builder for the endpoint.
105    pub fn builder() -> CreatePersonalAccessTokenForUserBuilder<'a> {
106        CreatePersonalAccessTokenForUserBuilder::default()
107    }
108}
109
110impl CreatePersonalAccessTokenForUserBuilder<'_> {
111    /// Add a scope for the token.
112    pub fn scope(&mut self, scope: PersonalAccessTokenScope) -> &mut Self {
113        self.scopes.get_or_insert_with(BTreeSet::new).insert(scope);
114        self
115    }
116
117    /// Add scopes for the token.
118    pub fn scopes<I>(&mut self, scopes: I) -> &mut Self
119    where
120        I: Iterator<Item = PersonalAccessTokenScope>,
121    {
122        self.scopes.get_or_insert_with(BTreeSet::new).extend(scopes);
123        self
124    }
125}
126
127impl Endpoint for CreatePersonalAccessTokenForUser<'_> {
128    fn method(&self) -> Method {
129        Method::POST
130    }
131
132    fn endpoint(&self) -> Cow<'static, str> {
133        format!("users/{}/personal_access_tokens", self.user).into()
134    }
135
136    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
137        let mut params = FormParams::default();
138
139        params
140            .push("name", &self.name)
141            .push_opt("description", self.description.as_ref())
142            .push_opt("expires_at", self.expires_at);
143
144        params.extend(self.scopes.iter().map(|&value| ("scopes[]", value)));
145
146        params.into_body()
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use chrono::NaiveDate;
153    use http::Method;
154
155    use crate::api::users::personal_access_tokens::{
156        CreatePersonalAccessTokenForUser, CreatePersonalAccessTokenForUserBuilderError,
157        PersonalAccessTokenCreateScope, PersonalAccessTokenScope,
158    };
159    use crate::api::{self, Query};
160    use crate::test::client::{ExpectedUrl, SingleTestClient};
161
162    #[test]
163    fn personal_access_token_scope_as_str() {
164        let items = &[
165            (PersonalAccessTokenScope::Api, "api"),
166            (PersonalAccessTokenScope::ReadUser, "read_user"),
167            (PersonalAccessTokenScope::ReadApi, "read_api"),
168            (PersonalAccessTokenScope::ReadRepository, "read_repository"),
169            (
170                PersonalAccessTokenScope::WriteRepository,
171                "write_repository",
172            ),
173            (PersonalAccessTokenScope::ReadRegistry, "read_registry"),
174            (PersonalAccessTokenScope::WriteRegistry, "write_registry"),
175            (PersonalAccessTokenScope::Sudo, "sudo"),
176            (PersonalAccessTokenScope::AdminMode, "admin_mode"),
177            (PersonalAccessTokenScope::CreateRunner, "create_runner"),
178            (PersonalAccessTokenScope::AiFeatures, "ai_features"),
179            (PersonalAccessTokenScope::K8sProxy, "k8s_proxy"),
180            (
181                PersonalAccessTokenScope::ReadServicePing,
182                "read_service_ping",
183            ),
184        ];
185
186        for (i, s) in items {
187            assert_eq!(i.as_str(), *s);
188        }
189    }
190
191    #[test]
192    fn personal_access_token_scope_as_create_scope() {
193        let items = &[
194            (PersonalAccessTokenScope::Api, None),
195            (PersonalAccessTokenScope::ReadUser, None),
196            (PersonalAccessTokenScope::ReadApi, None),
197            (PersonalAccessTokenScope::ReadRepository, None),
198            (PersonalAccessTokenScope::WriteRepository, None),
199            (PersonalAccessTokenScope::ReadRegistry, None),
200            (PersonalAccessTokenScope::WriteRegistry, None),
201            (PersonalAccessTokenScope::Sudo, None),
202            (PersonalAccessTokenScope::AdminMode, None),
203            (PersonalAccessTokenScope::CreateRunner, None),
204            (PersonalAccessTokenScope::AiFeatures, None),
205            (
206                PersonalAccessTokenScope::K8sProxy,
207                Some(PersonalAccessTokenCreateScope::K8sProxy),
208            ),
209            (PersonalAccessTokenScope::ReadServicePing, None),
210        ];
211
212        for (i, s) in items {
213            assert_eq!(i.as_create_scope(), *s);
214        }
215    }
216
217    #[test]
218    fn user_name_and_scopes_are_necessary() {
219        let err = CreatePersonalAccessTokenForUser::builder()
220            .build()
221            .unwrap_err();
222        crate::test::assert_missing_field!(
223            err,
224            CreatePersonalAccessTokenForUserBuilderError,
225            "user",
226        );
227    }
228
229    #[test]
230    fn user_is_necessary() {
231        let err = CreatePersonalAccessTokenForUser::builder()
232            .name("name")
233            .scope(PersonalAccessTokenScope::Api)
234            .build()
235            .unwrap_err();
236        crate::test::assert_missing_field!(
237            err,
238            CreatePersonalAccessTokenForUserBuilderError,
239            "user",
240        );
241    }
242
243    #[test]
244    fn name_is_necessary() {
245        let err = CreatePersonalAccessTokenForUser::builder()
246            .user(1)
247            .scope(PersonalAccessTokenScope::Api)
248            .build()
249            .unwrap_err();
250        crate::test::assert_missing_field!(
251            err,
252            CreatePersonalAccessTokenForUserBuilderError,
253            "name",
254        );
255    }
256
257    #[test]
258    fn scopes_is_necessary() {
259        let err = CreatePersonalAccessTokenForUser::builder()
260            .user(1)
261            .name("name")
262            .build()
263            .unwrap_err();
264        crate::test::assert_missing_field!(
265            err,
266            CreatePersonalAccessTokenForUserBuilderError,
267            "scopes",
268        );
269    }
270
271    #[test]
272    fn user_name_and_scopes_are_sufficient() {
273        CreatePersonalAccessTokenForUser::builder()
274            .user(1)
275            .name("name")
276            .scope(PersonalAccessTokenScope::ReadUser)
277            .build()
278            .unwrap();
279    }
280
281    #[test]
282    fn endpoint() {
283        let endpoint = ExpectedUrl::builder()
284            .method(Method::POST)
285            .endpoint("users/1/personal_access_tokens")
286            .content_type("application/x-www-form-urlencoded")
287            .body_str(concat!("name=name", "&scopes%5B%5D=api"))
288            .build()
289            .unwrap();
290        let client = SingleTestClient::new_raw(endpoint, "");
291
292        let endpoint = CreatePersonalAccessTokenForUser::builder()
293            .user(1)
294            .name("name")
295            .scopes([PersonalAccessTokenScope::Api].iter().cloned())
296            .build()
297            .unwrap();
298        api::ignore(endpoint).query(&client).unwrap();
299    }
300
301    #[test]
302    fn endpoint_description() {
303        let endpoint = ExpectedUrl::builder()
304            .method(Method::POST)
305            .endpoint("users/1/personal_access_tokens")
306            .content_type("application/x-www-form-urlencoded")
307            .body_str(concat!(
308                "name=name",
309                "&description=a+token+description",
310                "&scopes%5B%5D=api",
311            ))
312            .build()
313            .unwrap();
314        let client = SingleTestClient::new_raw(endpoint, "");
315
316        let endpoint = CreatePersonalAccessTokenForUser::builder()
317            .user(1)
318            .name("name")
319            .scope(PersonalAccessTokenScope::Api)
320            .description("a token description")
321            .build()
322            .unwrap();
323        api::ignore(endpoint).query(&client).unwrap();
324    }
325
326    #[test]
327    fn endpoint_expires_at() {
328        let endpoint = ExpectedUrl::builder()
329            .method(Method::POST)
330            .endpoint("users/1/personal_access_tokens")
331            .content_type("application/x-www-form-urlencoded")
332            .body_str(concat!(
333                "name=name",
334                "&expires_at=2022-01-01",
335                "&scopes%5B%5D=api",
336            ))
337            .build()
338            .unwrap();
339        let client = SingleTestClient::new_raw(endpoint, "");
340
341        let endpoint = CreatePersonalAccessTokenForUser::builder()
342            .user(1)
343            .name("name")
344            .scope(PersonalAccessTokenScope::Api)
345            .expires_at(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap())
346            .build()
347            .unwrap();
348        api::ignore(endpoint).query(&client).unwrap();
349    }
350}