gitlab/api/projects/access_tokens/
create.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::common::{AccessLevel, NameOrId};
13use crate::api::endpoint_prelude::*;
14use crate::api::ParamValue;
15
16/// Scopes for personal access tokens.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18#[non_exhaustive]
19pub enum ProjectAccessTokenScope {
20    /// Access the API and perform git reads and writes.
21    Api,
22    /// Access read-only API endpoints.
23    ReadApi,
24    /// Read access to repositories.
25    ReadRepository,
26    /// Write access to repositories.
27    WriteRepository,
28    /// Read access to Docker registries.
29    ReadRegistry,
30    /// Write access to Docker registries.
31    WriteRegistry,
32    /// Permission to create instance runners.
33    CreateRunner,
34    /// Access to AI features (GitLab Duo for JetBrains).
35    AiFeatures,
36    /// Access to perform Kubernetes API calls.
37    K8sProxy,
38}
39
40impl ProjectAccessTokenScope {
41    /// The scope as a query parameter.
42    pub(crate) fn as_str(self) -> &'static str {
43        match self {
44            Self::Api => "api",
45            Self::ReadApi => "read_api",
46            Self::ReadRepository => "read_repository",
47            Self::WriteRepository => "write_repository",
48            Self::ReadRegistry => "read_registry",
49            Self::WriteRegistry => "write_registry",
50            Self::CreateRunner => "create_runner",
51            Self::AiFeatures => "ai_features",
52            Self::K8sProxy => "k8s_proxy",
53        }
54    }
55}
56
57impl ParamValue<'static> for ProjectAccessTokenScope {
58    fn as_value(&self) -> Cow<'static, str> {
59        self.as_str().into()
60    }
61}
62
63/// Access levels for groups and projects.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
65#[non_exhaustive]
66pub enum ProjectAccessTokenAccessLevel {
67    /// Guest access (can see the project).
68    Guest,
69    /// Planner access (can manage issues).
70    Planner,
71    /// Reporter access (can open issues).
72    Reporter,
73    /// Developer access (can push branches, handle issues and merge requests).
74    Developer,
75    /// Maintainer access (can push to protected branches).
76    Maintainer,
77    /// Owner access (full rights).
78    Owner,
79}
80
81impl From<ProjectAccessTokenAccessLevel> for AccessLevel {
82    fn from(p: ProjectAccessTokenAccessLevel) -> Self {
83        match p {
84            ProjectAccessTokenAccessLevel::Guest => Self::Guest,
85            ProjectAccessTokenAccessLevel::Planner => Self::Planner,
86            ProjectAccessTokenAccessLevel::Reporter => Self::Reporter,
87            ProjectAccessTokenAccessLevel::Developer => Self::Developer,
88            ProjectAccessTokenAccessLevel::Maintainer => Self::Maintainer,
89            ProjectAccessTokenAccessLevel::Owner => Self::Owner,
90        }
91    }
92}
93
94/// Create a new personal access token for the authenticated user.
95#[derive(Debug, Builder, Clone)]
96#[builder(setter(strip_option))]
97pub struct CreateProjectAccessToken<'a> {
98    /// The project to create the access token for.
99    #[builder(setter(into))]
100    project: NameOrId<'a>,
101    /// The name of the personal access token.
102    #[builder(setter(into))]
103    name: Cow<'a, str>,
104    /// The scopes to allow the token to access.
105    #[builder(setter(name = "_scopes"), private)]
106    scopes: BTreeSet<ProjectAccessTokenScope>,
107
108    /// When the token expires.
109    #[builder(default)]
110    access_level: Option<ProjectAccessTokenAccessLevel>,
111    /// When the token expires.
112    #[builder(default)]
113    expires_at: Option<NaiveDate>,
114    /// An optional description for the access token.
115    #[builder(setter(into), default)]
116    description: Option<Cow<'a, str>>,
117}
118
119impl<'a> CreateProjectAccessToken<'a> {
120    /// Create a builder for the endpoint.
121    pub fn builder() -> CreateProjectAccessTokenBuilder<'a> {
122        CreateProjectAccessTokenBuilder::default()
123    }
124}
125
126impl CreateProjectAccessTokenBuilder<'_> {
127    /// Add a scope for the token.
128    pub fn scope(&mut self, scope: ProjectAccessTokenScope) -> &mut Self {
129        self.scopes.get_or_insert_with(BTreeSet::new).insert(scope);
130        self
131    }
132
133    /// Add scopes for the token.
134    pub fn scopes<I>(&mut self, scopes: I) -> &mut Self
135    where
136        I: Iterator<Item = ProjectAccessTokenScope>,
137    {
138        self.scopes.get_or_insert_with(BTreeSet::new).extend(scopes);
139        self
140    }
141}
142
143impl Endpoint for CreateProjectAccessToken<'_> {
144    fn method(&self) -> Method {
145        Method::POST
146    }
147
148    fn endpoint(&self) -> Cow<'static, str> {
149        format!("projects/{}/access_tokens", self.project).into()
150    }
151
152    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
153        let mut params = FormParams::default();
154
155        params
156            .push("name", &self.name)
157            .push_opt(
158                "access_level",
159                self.access_level.map(|a| AccessLevel::from(a).as_u64()),
160            )
161            .push_opt("expires_at", self.expires_at)
162            .push_opt("description", self.description.as_ref());
163
164        params.extend(self.scopes.iter().map(|&value| ("scopes[]", value)));
165
166        params.into_body()
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use chrono::NaiveDate;
173    use http::Method;
174
175    use crate::api::projects::access_tokens::{
176        CreateProjectAccessToken, CreateProjectAccessTokenBuilderError,
177        ProjectAccessTokenAccessLevel, ProjectAccessTokenScope,
178    };
179    use crate::api::{self, Query};
180    use crate::test::client::{ExpectedUrl, SingleTestClient};
181
182    #[test]
183    fn personal_access_token_create_scope_as_str() {
184        let items = &[
185            (ProjectAccessTokenScope::Api, "api"),
186            (ProjectAccessTokenScope::ReadApi, "read_api"),
187            (ProjectAccessTokenScope::ReadRepository, "read_repository"),
188            (ProjectAccessTokenScope::WriteRepository, "write_repository"),
189            (ProjectAccessTokenScope::ReadRegistry, "read_registry"),
190            (ProjectAccessTokenScope::WriteRegistry, "write_registry"),
191            (ProjectAccessTokenScope::CreateRunner, "create_runner"),
192            (ProjectAccessTokenScope::AiFeatures, "ai_features"),
193            (ProjectAccessTokenScope::K8sProxy, "k8s_proxy"),
194        ];
195
196        for (i, s) in items {
197            assert_eq!(i.as_str(), *s);
198        }
199    }
200
201    #[test]
202    fn project_token_access_level_from() {
203        use crate::api::common::AccessLevel;
204
205        let items = &[
206            (ProjectAccessTokenAccessLevel::Guest, AccessLevel::Guest),
207            (ProjectAccessTokenAccessLevel::Planner, AccessLevel::Planner),
208            (
209                ProjectAccessTokenAccessLevel::Reporter,
210                AccessLevel::Reporter,
211            ),
212            (
213                ProjectAccessTokenAccessLevel::Developer,
214                AccessLevel::Developer,
215            ),
216            (
217                ProjectAccessTokenAccessLevel::Maintainer,
218                AccessLevel::Maintainer,
219            ),
220            (ProjectAccessTokenAccessLevel::Owner, AccessLevel::Owner),
221        ];
222
223        for (i, s) in items {
224            assert_eq!(AccessLevel::from(*i), *s);
225        }
226    }
227
228    #[test]
229    fn project_name_and_scopes_are_necessary() {
230        let err = CreateProjectAccessToken::builder().build().unwrap_err();
231        crate::test::assert_missing_field!(err, CreateProjectAccessTokenBuilderError, "project");
232    }
233
234    #[test]
235    fn project_is_necessary() {
236        let err = CreateProjectAccessToken::builder()
237            .name("name")
238            .scope(ProjectAccessTokenScope::K8sProxy)
239            .build()
240            .unwrap_err();
241        crate::test::assert_missing_field!(err, CreateProjectAccessTokenBuilderError, "project");
242    }
243
244    #[test]
245    fn name_is_necessary() {
246        let err = CreateProjectAccessToken::builder()
247            .project(1)
248            .scope(ProjectAccessTokenScope::K8sProxy)
249            .build()
250            .unwrap_err();
251        crate::test::assert_missing_field!(err, CreateProjectAccessTokenBuilderError, "name");
252    }
253
254    #[test]
255    fn scopes_is_necessary() {
256        let err = CreateProjectAccessToken::builder()
257            .project(1)
258            .name("name")
259            .build()
260            .unwrap_err();
261        crate::test::assert_missing_field!(err, CreateProjectAccessTokenBuilderError, "scopes");
262    }
263
264    #[test]
265    fn project_name_and_scopes_are_sufficient() {
266        CreateProjectAccessToken::builder()
267            .project(1)
268            .name("name")
269            .scope(ProjectAccessTokenScope::K8sProxy)
270            .build()
271            .unwrap();
272    }
273
274    #[test]
275    fn endpoint() {
276        let endpoint = ExpectedUrl::builder()
277            .method(Method::POST)
278            .endpoint("projects/1/access_tokens")
279            .content_type("application/x-www-form-urlencoded")
280            .body_str(concat!("name=name", "&scopes%5B%5D=k8s_proxy"))
281            .build()
282            .unwrap();
283        let client = SingleTestClient::new_raw(endpoint, "");
284
285        let endpoint = CreateProjectAccessToken::builder()
286            .project(1)
287            .name("name")
288            .scopes([ProjectAccessTokenScope::K8sProxy].iter().cloned())
289            .build()
290            .unwrap();
291        api::ignore(endpoint).query(&client).unwrap();
292    }
293
294    #[test]
295    fn endpoint_access_level() {
296        let endpoint = ExpectedUrl::builder()
297            .method(Method::POST)
298            .endpoint("projects/1/access_tokens")
299            .content_type("application/x-www-form-urlencoded")
300            .body_str(concat!(
301                "name=name",
302                "&access_level=30",
303                "&scopes%5B%5D=k8s_proxy",
304            ))
305            .build()
306            .unwrap();
307        let client = SingleTestClient::new_raw(endpoint, "");
308
309        let endpoint = CreateProjectAccessToken::builder()
310            .project(1)
311            .name("name")
312            .scope(ProjectAccessTokenScope::K8sProxy)
313            .access_level(ProjectAccessTokenAccessLevel::Developer)
314            .build()
315            .unwrap();
316        api::ignore(endpoint).query(&client).unwrap();
317    }
318
319    #[test]
320    fn endpoint_expires_at() {
321        let endpoint = ExpectedUrl::builder()
322            .method(Method::POST)
323            .endpoint("projects/1/access_tokens")
324            .content_type("application/x-www-form-urlencoded")
325            .body_str(concat!(
326                "name=name",
327                "&expires_at=2022-01-01",
328                "&scopes%5B%5D=k8s_proxy",
329            ))
330            .build()
331            .unwrap();
332        let client = SingleTestClient::new_raw(endpoint, "");
333
334        let endpoint = CreateProjectAccessToken::builder()
335            .project(1)
336            .name("name")
337            .scope(ProjectAccessTokenScope::K8sProxy)
338            .expires_at(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap())
339            .build()
340            .unwrap();
341        api::ignore(endpoint).query(&client).unwrap();
342    }
343
344    #[test]
345    fn endpoint_description() {
346        let endpoint = ExpectedUrl::builder()
347            .method(Method::POST)
348            .endpoint("projects/1/access_tokens")
349            .content_type("application/x-www-form-urlencoded")
350            .body_str(concat!(
351                "name=name",
352                "&description=a+description",
353                "&scopes%5B%5D=k8s_proxy",
354            ))
355            .build()
356            .unwrap();
357        let client = SingleTestClient::new_raw(endpoint, "");
358
359        let endpoint = CreateProjectAccessToken::builder()
360            .project(1)
361            .name("name")
362            .scope(ProjectAccessTokenScope::K8sProxy)
363            .description("a description")
364            .build()
365            .unwrap();
366        api::ignore(endpoint).query(&client).unwrap();
367    }
368}