-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgitlab.rs
174 lines (156 loc) · 6.32 KB
/
gitlab.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
use crate::{SshPublicKey, USER_AGENT};
use reqwest::{Client, Result, Url};
use serde::Deserialize;
#[derive(Debug)]
pub struct Gitlab {
/// The base URL of the API.
base_url: Url,
}
impl Gitlab {
const VERSION: &'static str = "v4";
const ACCEPT_HEADER: &'static str = "application/json";
pub fn new(base_url: Url) -> Self {
Self { base_url }
}
/// Get the signing keys of a user by their username.
///
/// # API documentation
/// https://docs.gitlab.com/16.10/ee/api/users.html#list-ssh-keys-for-user
pub async fn get_keys_by_username(
&self,
username: &str,
client: &Client,
) -> Result<Vec<SshPublicKey>> {
let url = self
.base_url
.join(&format!(
"/api/{version}/users/{username}/keys",
version = Self::VERSION,
))
.unwrap();
let request = client
.get(url)
.header("User-Agent", USER_AGENT)
.header("Accept", Self::ACCEPT_HEADER);
let response = request.send().await?;
// The API has no way to filter keys by usage type, so this contains all the user's keys.
let all_keys: Vec<ApiSshKey> = response.json().await?;
// Filter out the keys that are not used for signing.
let signing_keys = all_keys
.into_iter()
.filter(|key| key.usage_type.is_signing());
Ok(signing_keys.map(SshPublicKey::from).collect())
}
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub enum ApiSshKeyUsage {
#[serde(rename = "auth")]
Auth,
#[serde(rename = "signing")]
Signing,
#[serde(rename = "auth_and_signing")]
AuthAndSigning,
}
impl ApiSshKeyUsage {
/// Returns true if the key is used for signing.
pub fn is_signing(&self) -> bool {
matches!(
self,
ApiSshKeyUsage::Signing | ApiSshKeyUsage::AuthAndSigning
)
}
}
/// The GitLab API representation of an SSH key.
#[derive(Debug, Deserialize)]
pub struct ApiSshKey {
pub id: usize,
pub title: String,
pub key: String,
pub usage_type: ApiSshKeyUsage,
}
impl From<ApiSshKey> for SshPublicKey {
fn from(api_key: ApiSshKey) -> Self {
api_key.key.parse().unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
use httpmock::prelude::*;
use rstest::rstest;
const API_ACCEPT_HEADER: &str = "application/json";
/// The API request made to get a users signing keys is correct.
#[tokio::test]
async fn api_request_is_correct() {
let username = "tanuki";
let server = MockServer::start();
let mock = server.mock(|when, _| {
when.method(GET)
.path(format!("/api/v4/users/{username}/keys"))
.header("accept", API_ACCEPT_HEADER)
.header("user-agent", USER_AGENT);
});
let client = Client::new();
let api = Gitlab {
base_url: server.base_url().parse().unwrap(),
};
let _ = api.get_keys_by_username(username, &client).await;
mock.assert();
}
/// Keys returned from the API are deserialized correctly.
#[rstest]
#[case("[]", vec![])]
#[case(
r#"[
{
"id": 1121029,
"title": "key-1",
"created_at": "2020-08-21T19:43:06.816Z",
"expires_at": null,
"key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGtQUDZWhs8k/cZcykMkaoX7ZE7DXld8TP79HyddMVTS John Doe (gitlab.com)",
"usage_type": "auth_and_signing"
},
{
"id": 1121030,
"title": "key-2",
"created_at": "2023-07-22T23:04:29.415Z",
"expires_at": "2025-04-10T00:00:00.000Z",
"key": "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCoObGvI0R2SfxLypsqi25QOgiI1lcsAhtL7AqUeVD+4mS0CQ2Nu/C8h+RHtX6tHpd+GhfGjtDXjW598Vr2j9+w= John Doe (gitlab.com)",
"usage_type": "auth"
},
{
"id": 1121031,
"title": "key-3",
"created_at": "2023-12-04T19:32:23.794Z",
"expires_at": null,
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDDTdEeUFjUX76aMptdG63itqcINvu/tnV5l9RXy/1TS25Ui2r+C2pRjG0vr9lzfz8TGncQt1yKmaZDAAe6mYGFiQlrkh9RJ/MPssRw4uS4slvMTDWhNufO1M3QGkek81lGaZq55uazCcaM5xSOhLBdrWIMROeLgKZ9YkHNqJXTt9V+xNE5ZkB/65i2tCkGdXnQsGJbYFbkuUTvYBuMW9lwmryLTeWwFLWGBP1moZI9etk3snh2hCLTV8+gvmhCTE8sAGBMcJq+TGxnfFoCtnA9Bdy7t+ZMLh1kV7oneUA9YT7qNeUFy55D287DAltB02ntT7CtuG6SBAQ4CQMcCoAX3Os4aVfdILOEC8ghrAj3uTEQuE3nYta0SmqqXcVAxmXUQCawf8n5CJ7QN5aIhCH73MKr6k5puk9dnkAcAFLRM6stvQhnpIqrI3YEbjqs1FGHfbc4+nfEWorxRrd7ur1ckEhuvmAXRKrLzYp9gYWU6TxfRqSxsXh3he0G6i+kC6k= John Doe (gitlab.com)",
"usage_type": "signing"
}
]"#,
vec![
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGtQUDZWhs8k/cZcykMkaoX7ZE7DXld8TP79HyddMVTS John Doe (gitlab.com)".parse().unwrap(),
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDDTdEeUFjUX76aMptdG63itqcINvu/tnV5l9RXy/1TS25Ui2r+C2pRjG0vr9lzfz8TGncQt1yKmaZDAAe6mYGFiQlrkh9RJ/MPssRw4uS4slvMTDWhNufO1M3QGkek81lGaZq55uazCcaM5xSOhLBdrWIMROeLgKZ9YkHNqJXTt9V+xNE5ZkB/65i2tCkGdXnQsGJbYFbkuUTvYBuMW9lwmryLTeWwFLWGBP1moZI9etk3snh2hCLTV8+gvmhCTE8sAGBMcJq+TGxnfFoCtnA9Bdy7t+ZMLh1kV7oneUA9YT7qNeUFy55D287DAltB02ntT7CtuG6SBAQ4CQMcCoAX3Os4aVfdILOEC8ghrAj3uTEQuE3nYta0SmqqXcVAxmXUQCawf8n5CJ7QN5aIhCH73MKr6k5puk9dnkAcAFLRM6stvQhnpIqrI3YEbjqs1FGHfbc4+nfEWorxRrd7ur1ckEhuvmAXRKrLzYp9gYWU6TxfRqSxsXh3he0G6i+kC6k= John Doe (gitlab.com)".parse().unwrap(),
]
)]
#[tokio::test]
async fn keys_returned_by_api_deserialized_correctly(
#[case] body: &str,
#[case] expected: Vec<SshPublicKey>,
) {
let username = "tanuki";
let server = MockServer::start();
server.mock(|when, then| {
when.method(GET)
.path(format!("/api/v4/users/{username}/keys"));
then.status(200)
.header("Content-Type", "application/json")
.body(body);
});
let client = Client::new();
let api = Gitlab {
base_url: server.base_url().parse().unwrap(),
};
let keys = api.get_keys_by_username(username, &client).await.unwrap();
assert_eq!(keys, expected);
}
}