Skip to content

Commit bac756e

Browse files
authored
Add crate_scopes and endpoint_scopes fields to the PUT /me/tokens API endpoint (#5973)
1 parent cd34f0a commit bac756e

File tree

2 files changed

+137
-1
lines changed

2 files changed

+137
-1
lines changed

src/controllers/token.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::schema::api_tokens;
55
use crate::views::EncodableApiTokenWithToken;
66

77
use crate::auth::AuthCheck;
8+
use crate::models::token::{CrateScope, EndpointScope};
89
use axum::response::IntoResponse;
910
use serde_json as json;
1011

@@ -32,6 +33,8 @@ pub async fn new(app: AppState, req: BytesRequest) -> AppResult<Json<Value>> {
3233
#[derive(Deserialize, Serialize)]
3334
struct NewApiToken {
3435
name: String,
36+
crate_scopes: Option<Vec<String>>,
37+
endpoint_scopes: Option<Vec<String>>,
3538
}
3639

3740
/// The incoming serialization format for the `ApiToken` model.
@@ -67,7 +70,32 @@ pub async fn new(app: AppState, req: BytesRequest) -> AppResult<Json<Value>> {
6770
)));
6871
}
6972

70-
let api_token = ApiToken::insert(&conn, user.id, name)?;
73+
let crate_scopes = new
74+
.api_token
75+
.crate_scopes
76+
.map(|scopes| {
77+
scopes
78+
.into_iter()
79+
.map(CrateScope::try_from)
80+
.collect::<Result<Vec<_>, _>>()
81+
})
82+
.transpose()
83+
.map_err(|_err| bad_request("invalid crate scope"))?;
84+
85+
let endpoint_scopes = new
86+
.api_token
87+
.endpoint_scopes
88+
.map(|scopes| {
89+
scopes
90+
.into_iter()
91+
.map(|scope| EndpointScope::try_from(scope.as_bytes()))
92+
.collect::<Result<Vec<_>, _>>()
93+
})
94+
.transpose()
95+
.map_err(|_err| bad_request("invalid endpoint scope"))?;
96+
97+
let api_token =
98+
ApiToken::insert_with_scopes(&conn, user.id, name, crate_scopes, endpoint_scopes)?;
7199
let api_token = EncodableApiTokenWithToken::from(api_token);
72100

73101
Ok(Json(json!({ "api_token": api_token })))

src/tests/routes/me/tokens/create.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::util::{RequestHelper, TestApp};
2+
use cargo_registry::models::token::{CrateScope, EndpointScope};
23
use cargo_registry::models::ApiToken;
34
use cargo_registry::views::EncodableApiTokenWithToken;
45
use diesel::prelude::*;
@@ -72,6 +73,8 @@ fn create_token_success() {
7273
assert_eq!(tokens[0].name, "bar");
7374
assert!(!tokens[0].revoked);
7475
assert_eq!(tokens[0].last_used_at, None);
76+
assert_eq!(tokens[0].crate_scopes, None);
77+
assert_eq!(tokens[0].endpoint_scopes, None);
7578
}
7679

7780
#[test]
@@ -107,3 +110,108 @@ fn cannot_create_token_with_token() {
107110
json!({ "errors": [{ "detail": "cannot use an API token to create a new API token" }] })
108111
);
109112
}
113+
114+
#[test]
115+
fn create_token_with_scopes() {
116+
let (app, _, user) = TestApp::init().with_user();
117+
118+
let json = json!({
119+
"api_token": {
120+
"name": "bar",
121+
"crate_scopes": ["tokio", "tokio-*"],
122+
"endpoint_scopes": ["publish-update"],
123+
}
124+
});
125+
126+
let json: NewResponse = user
127+
.put("/api/v1/me/tokens", &serde_json::to_vec(&json).unwrap())
128+
.good();
129+
assert_eq!(json.api_token.name, "bar");
130+
assert!(!json.api_token.token.is_empty());
131+
132+
let tokens: Vec<ApiToken> =
133+
app.db(|conn| assert_ok!(ApiToken::belonging_to(user.as_model()).load(conn)));
134+
assert_eq!(tokens.len(), 1);
135+
assert_eq!(tokens[0].name, "bar");
136+
assert!(!tokens[0].revoked);
137+
assert_eq!(tokens[0].last_used_at, None);
138+
assert_eq!(
139+
tokens[0].crate_scopes,
140+
Some(vec![
141+
CrateScope::try_from("tokio").unwrap(),
142+
CrateScope::try_from("tokio-*").unwrap()
143+
])
144+
);
145+
assert_eq!(
146+
tokens[0].endpoint_scopes,
147+
Some(vec![EndpointScope::PublishUpdate])
148+
);
149+
}
150+
151+
#[test]
152+
fn create_token_with_null_scopes() {
153+
let (app, _, user) = TestApp::init().with_user();
154+
155+
let json = json!({
156+
"api_token": {
157+
"name": "bar",
158+
"crate_scopes": null,
159+
"endpoint_scopes": null,
160+
}
161+
});
162+
163+
let json: NewResponse = user
164+
.put("/api/v1/me/tokens", &serde_json::to_vec(&json).unwrap())
165+
.good();
166+
assert_eq!(json.api_token.name, "bar");
167+
assert!(!json.api_token.token.is_empty());
168+
169+
let tokens: Vec<ApiToken> =
170+
app.db(|conn| assert_ok!(ApiToken::belonging_to(user.as_model()).load(conn)));
171+
assert_eq!(tokens.len(), 1);
172+
assert_eq!(tokens[0].name, "bar");
173+
assert!(!tokens[0].revoked);
174+
assert_eq!(tokens[0].last_used_at, None);
175+
assert_eq!(tokens[0].crate_scopes, None);
176+
assert_eq!(tokens[0].endpoint_scopes, None);
177+
}
178+
179+
#[test]
180+
fn create_token_with_empty_crate_scope() {
181+
let (_, _, user) = TestApp::init().with_user();
182+
183+
let json = json!({
184+
"api_token": {
185+
"name": "bar",
186+
"crate_scopes": ["", "tokio-*"],
187+
"endpoint_scopes": ["publish-update"],
188+
}
189+
});
190+
191+
let response = user.put::<()>("/api/v1/me/tokens", &serde_json::to_vec(&json).unwrap());
192+
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
193+
assert_eq!(
194+
response.into_json(),
195+
json!({ "errors": [{ "detail": "invalid crate scope" }] })
196+
);
197+
}
198+
199+
#[test]
200+
fn create_token_with_invalid_endpoint_scope() {
201+
let (_, _, user) = TestApp::init().with_user();
202+
203+
let json = json!({
204+
"api_token": {
205+
"name": "bar",
206+
"crate_scopes": ["tokio", "tokio-*"],
207+
"endpoint_scopes": ["crash"],
208+
}
209+
});
210+
211+
let response = user.put::<()>("/api/v1/me/tokens", &serde_json::to_vec(&json).unwrap());
212+
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
213+
assert_eq!(
214+
response.into_json(),
215+
json!({ "errors": [{ "detail": "invalid endpoint scope" }] })
216+
);
217+
}

0 commit comments

Comments
 (0)