Skip to main content

gitlab/api/users/impersonation_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::endpoint_prelude::*;
13use crate::api::ParamValue;
14
15/// Scopes for impersonation tokens.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17#[non_exhaustive]
18pub enum ImpersonationTokenScope {
19    /// Access the API and perform git reads and writes.
20    Api,
21    /// Access to read the user information.
22    ReadUser,
23}
24
25impl ImpersonationTokenScope {
26    /// The scope as a query parameter.
27    pub(crate) fn as_str(self) -> &'static str {
28        match self {
29            ImpersonationTokenScope::Api => "api",
30            ImpersonationTokenScope::ReadUser => "read_user",
31        }
32    }
33}
34
35impl ParamValue<'static> for ImpersonationTokenScope {
36    fn as_value(&self) -> Cow<'static, str> {
37        self.as_str().into()
38    }
39}
40
41/// Create a new impersonation token for a user.
42#[derive(Debug, Builder, Clone)]
43#[builder(setter(strip_option))]
44pub struct CreateImpersonationToken<'a> {
45    /// The user to create an impersonation token for.
46    user: u64,
47    /// The name of the impersonation token.
48    #[builder(setter(into))]
49    name: Cow<'a, str>,
50    /// The scopes to allow the token to access.
51    #[builder(setter(name = "_scopes"), private)]
52    scopes: BTreeSet<ImpersonationTokenScope>,
53
54    /// An optional description for the impersonation token.
55    #[builder(setter(into), default)]
56    description: Option<Cow<'a, str>>,
57    /// When the token expires.
58    #[builder(default)]
59    expires_at: Option<NaiveDate>,
60}
61
62impl<'a> CreateImpersonationToken<'a> {
63    /// Create a builder for the endpoint.
64    pub fn builder() -> CreateImpersonationTokenBuilder<'a> {
65        CreateImpersonationTokenBuilder::default()
66    }
67}
68
69impl CreateImpersonationTokenBuilder<'_> {
70    /// Add a scope for the token.
71    pub fn scope(&mut self, scope: ImpersonationTokenScope) -> &mut Self {
72        self.scopes.get_or_insert_with(BTreeSet::new).insert(scope);
73        self
74    }
75
76    /// Add scopes for the token.
77    pub fn scopes<I>(&mut self, scopes: I) -> &mut Self
78    where
79        I: Iterator<Item = ImpersonationTokenScope>,
80    {
81        self.scopes.get_or_insert_with(BTreeSet::new).extend(scopes);
82        self
83    }
84}
85
86impl Endpoint for CreateImpersonationToken<'_> {
87    fn method(&self) -> Method {
88        Method::POST
89    }
90
91    fn endpoint(&self) -> Cow<'static, str> {
92        format!("users/{}/impersonation_tokens", self.user).into()
93    }
94
95    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
96        let mut params = FormParams::default();
97
98        params
99            .push("name", &self.name)
100            .push_opt("description", self.description.as_ref())
101            .push_opt("expires_at", self.expires_at);
102
103        params.extend(self.scopes.iter().map(|&value| ("scopes[]", value)));
104
105        params.into_body()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use chrono::NaiveDate;
112    use http::Method;
113
114    use crate::api::users::impersonation_tokens::{
115        CreateImpersonationToken, CreateImpersonationTokenBuilderError, ImpersonationTokenScope,
116    };
117    use crate::api::{self, Query};
118    use crate::test::client::{ExpectedUrl, SingleTestClient};
119
120    #[test]
121    fn impersonation_token_scope_as_str() {
122        let items = &[
123            (ImpersonationTokenScope::Api, "api"),
124            (ImpersonationTokenScope::ReadUser, "read_user"),
125        ];
126
127        for (i, s) in items {
128            assert_eq!(i.as_str(), *s);
129        }
130    }
131
132    #[test]
133    fn user_name_and_scopes_are_necessary() {
134        let err = CreateImpersonationToken::builder().build().unwrap_err();
135        crate::test::assert_missing_field!(err, CreateImpersonationTokenBuilderError, "user");
136    }
137
138    #[test]
139    fn user_is_necessary() {
140        let err = CreateImpersonationToken::builder()
141            .name("name")
142            .scope(ImpersonationTokenScope::Api)
143            .build()
144            .unwrap_err();
145        crate::test::assert_missing_field!(err, CreateImpersonationTokenBuilderError, "user");
146    }
147
148    #[test]
149    fn name_is_necessary() {
150        let err = CreateImpersonationToken::builder()
151            .user(1)
152            .scope(ImpersonationTokenScope::Api)
153            .build()
154            .unwrap_err();
155        crate::test::assert_missing_field!(err, CreateImpersonationTokenBuilderError, "name");
156    }
157
158    #[test]
159    fn scopes_is_necessary() {
160        let err = CreateImpersonationToken::builder()
161            .user(1)
162            .name("name")
163            .build()
164            .unwrap_err();
165        crate::test::assert_missing_field!(err, CreateImpersonationTokenBuilderError, "scopes");
166    }
167
168    #[test]
169    fn user_name_and_scopes_are_sufficient() {
170        CreateImpersonationToken::builder()
171            .user(1)
172            .name("name")
173            .scope(ImpersonationTokenScope::ReadUser)
174            .build()
175            .unwrap();
176    }
177
178    #[test]
179    fn endpoint() {
180        let endpoint = ExpectedUrl::builder()
181            .method(Method::POST)
182            .endpoint("users/1/impersonation_tokens")
183            .content_type("application/x-www-form-urlencoded")
184            .body_str(concat!("name=name", "&scopes%5B%5D=api"))
185            .build()
186            .unwrap();
187        let client = SingleTestClient::new_raw(endpoint, "");
188
189        let endpoint = CreateImpersonationToken::builder()
190            .user(1)
191            .name("name")
192            .scopes([ImpersonationTokenScope::Api].iter().cloned())
193            .build()
194            .unwrap();
195        api::ignore(endpoint).query(&client).unwrap();
196    }
197
198    #[test]
199    fn endpoint_description() {
200        let endpoint = ExpectedUrl::builder()
201            .method(Method::POST)
202            .endpoint("users/1/impersonation_tokens")
203            .content_type("application/x-www-form-urlencoded")
204            .body_str(concat!(
205                "name=name",
206                "&description=a+token+description",
207                "&scopes%5B%5D=api",
208            ))
209            .build()
210            .unwrap();
211        let client = SingleTestClient::new_raw(endpoint, "");
212
213        let endpoint = CreateImpersonationToken::builder()
214            .user(1)
215            .name("name")
216            .scope(ImpersonationTokenScope::Api)
217            .description("a token description")
218            .build()
219            .unwrap();
220        api::ignore(endpoint).query(&client).unwrap();
221    }
222
223    #[test]
224    fn endpoint_expires_at() {
225        let endpoint = ExpectedUrl::builder()
226            .method(Method::POST)
227            .endpoint("users/1/impersonation_tokens")
228            .content_type("application/x-www-form-urlencoded")
229            .body_str(concat!(
230                "name=name",
231                "&expires_at=2022-01-01",
232                "&scopes%5B%5D=api",
233            ))
234            .build()
235            .unwrap();
236        let client = SingleTestClient::new_raw(endpoint, "");
237
238        let endpoint = CreateImpersonationToken::builder()
239            .user(1)
240            .name("name")
241            .scope(ImpersonationTokenScope::Api)
242            .expires_at(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap())
243            .build()
244            .unwrap();
245        api::ignore(endpoint).query(&client).unwrap();
246    }
247}