Skip to content

Commit 99f1ae4

Browse files
authored
Merge pull request #12433 from Turbo87/trustpub-list-by-user
trustpub/list: Add support for `user_id` query parameter
2 parents dd5d749 + 3e8a7ed commit 99f1ae4

File tree

44 files changed

+1426
-114
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1426
-114
lines changed

src/auth.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub struct AuthCheck {
6868
allow_token: bool,
6969
endpoint_scope: Option<EndpointScope>,
7070
crate_name: Option<String>,
71+
allow_any_crate_scope: bool,
7172
}
7273

7374
impl AuthCheck {
@@ -79,6 +80,7 @@ impl AuthCheck {
7980
allow_token: true,
8081
endpoint_scope: None,
8182
crate_name: None,
83+
allow_any_crate_scope: false,
8284
}
8385
}
8486

@@ -88,6 +90,7 @@ impl AuthCheck {
8890
allow_token: false,
8991
endpoint_scope: None,
9092
crate_name: None,
93+
allow_any_crate_scope: false,
9194
}
9295
}
9396

@@ -96,6 +99,7 @@ impl AuthCheck {
9699
allow_token: self.allow_token,
97100
endpoint_scope: Some(endpoint_scope),
98101
crate_name: self.crate_name.clone(),
102+
allow_any_crate_scope: self.allow_any_crate_scope,
99103
}
100104
}
101105

@@ -104,6 +108,20 @@ impl AuthCheck {
104108
allow_token: self.allow_token,
105109
endpoint_scope: self.endpoint_scope,
106110
crate_name: Some(crate_name.to_string()),
111+
allow_any_crate_scope: self.allow_any_crate_scope,
112+
}
113+
}
114+
115+
/// Allow tokens with any crate scope without specifying a particular crate.
116+
///
117+
/// Use this for endpoints that deal with multiple crates at once, where the
118+
/// caller will handle crate scope filtering manually.
119+
pub fn allow_any_crate_scope(&self) -> Self {
120+
Self {
121+
allow_token: self.allow_token,
122+
endpoint_scope: self.endpoint_scope,
123+
crate_name: self.crate_name.clone(),
124+
allow_any_crate_scope: true,
107125
}
108126
}
109127

@@ -170,7 +188,8 @@ impl AuthCheck {
170188
(Some(token_scopes), _) if token_scopes.is_empty() => true,
171189

172190
// The token has crate scopes, but the endpoint does not deal with crates.
173-
(Some(_), None) => false,
191+
// However, if allow_any_crate_scope is set, we allow it (caller handles filtering).
192+
(Some(_), None) => self.allow_any_crate_scope,
174193

175194
// The token is NOT a legacy token, and the endpoint allows a certain endpoint scope or a legacy token.
176195
(Some(token_scopes), Some(crate_name)) => token_scopes

src/controllers/trustpub/github_configs/list.rs

Lines changed: 124 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ use crate::controllers::helpers::pagination::{
66
use crate::controllers::krate::load_crate;
77
use crate::controllers::trustpub::github_configs::json::{self, ListResponse, ListResponseMeta};
88
use crate::util::RequestUtils;
9-
use crate::util::errors::{AppResult, bad_request};
9+
use crate::util::errors::{AppResult, bad_request, forbidden};
1010
use axum::Json;
1111
use axum::extract::{FromRequestParts, Query};
1212
use crates_io_database::models::OwnerKind;
1313
use crates_io_database::models::token::EndpointScope;
1414
use crates_io_database::models::trustpub::GitHubConfig;
15-
use crates_io_database::schema::{crate_owners, trustpub_configs_github};
15+
use crates_io_database::schema::{crate_owners, crates, trustpub_configs_github};
1616
use diesel::dsl::{exists, select};
1717
use diesel::prelude::*;
1818
use diesel_async::RunQueryDsl;
@@ -26,7 +26,10 @@ use serde::Deserialize;
2626
pub struct ListQueryParams {
2727
/// Name of the crate to list Trusted Publishing configurations for.
2828
#[serde(rename = "crate")]
29-
pub krate: String,
29+
pub krate: Option<String>,
30+
31+
/// User ID to list Trusted Publishing configurations for all crates owned by the user.
32+
pub user_id: Option<i32>,
3033
}
3134

3235
/// List Trusted Publishing configurations for GitHub Actions.
@@ -42,17 +45,34 @@ pub async fn list_trustpub_github_configs(
4245
state: AppState,
4346
params: ListQueryParams,
4447
parts: Parts,
48+
) -> AppResult<Json<ListResponse>> {
49+
match (&params.krate, params.user_id) {
50+
(Some(krate), None) => list_by_crate(state, krate, parts).await,
51+
(None, Some(user_id)) => list_by_user(state, user_id, parts).await,
52+
(Some(_), Some(_)) => Err(bad_request(
53+
"Cannot specify both `crate` and `user_id` query parameters",
54+
)),
55+
(None, None) => Err(bad_request(
56+
"Must specify either `crate` or `user_id` query parameter",
57+
)),
58+
}
59+
}
60+
61+
async fn list_by_crate(
62+
state: AppState,
63+
krate_name: &str,
64+
parts: Parts,
4565
) -> AppResult<Json<ListResponse>> {
4666
let mut conn = state.db_read().await?;
4767

4868
let auth = AuthCheck::default()
4969
.with_endpoint_scope(EndpointScope::TrustedPublishing)
50-
.for_crate(&params.krate)
70+
.for_crate(krate_name)
5171
.check(&parts, &mut conn)
5272
.await?;
5373
let auth_user = auth.user();
5474

55-
let krate = load_crate(&mut conn, &params.krate).await?;
75+
let krate = load_crate(&mut conn, krate_name).await?;
5676

5777
// Check if the authenticated user is an owner of the crate
5878
let is_owner = select(exists(
@@ -69,40 +89,111 @@ pub async fn list_trustpub_github_configs(
6989
return Err(bad_request("You are not an owner of this crate"));
7090
}
7191

92+
paginated_response(&mut conn, &[krate.id], &parts).await
93+
}
94+
95+
async fn list_by_user(
96+
state: AppState,
97+
user_id: i32,
98+
parts: Parts,
99+
) -> AppResult<Json<ListResponse>> {
100+
let mut conn = state.db_read().await?;
101+
102+
let auth = AuthCheck::default()
103+
.with_endpoint_scope(EndpointScope::TrustedPublishing)
104+
.allow_any_crate_scope()
105+
.check(&parts, &mut conn)
106+
.await?;
107+
108+
// Reject legacy tokens for this endpoint
109+
auth.reject_legacy_tokens()?;
110+
111+
let auth_user = auth.user();
112+
113+
// Verify the authenticated user matches the requested user_id
114+
if auth_user.id != user_id {
115+
return Err(forbidden(
116+
"this action requires authentication as the specified user",
117+
));
118+
}
119+
120+
// Get crate scopes from the token (if any)
121+
let crate_scopes = auth.api_token().and_then(|t| t.crate_scopes.as_ref());
122+
123+
// Get all crate IDs owned by the user
124+
let mut owned_crates: Vec<(i32, String)> = crate_owners::table
125+
.inner_join(crates::table)
126+
.filter(crate_owners::owner_id.eq(user_id))
127+
.filter(crate_owners::owner_kind.eq(OwnerKind::User))
128+
.filter(crate_owners::deleted.eq(false))
129+
.select((crates::id, crates::name))
130+
.load(&mut conn)
131+
.await?;
132+
133+
// Filter by crate scopes if the token has any
134+
if let Some(scopes) = crate_scopes
135+
&& !scopes.is_empty()
136+
{
137+
owned_crates.retain(|(_, name)| scopes.iter().any(|scope| scope.matches(name)));
138+
}
139+
140+
let crate_ids: Vec<i32> = owned_crates.iter().map(|(id, _)| *id).collect();
141+
142+
paginated_response(&mut conn, &crate_ids, &parts).await
143+
}
144+
145+
async fn paginated_response(
146+
conn: &mut diesel_async::AsyncPgConnection,
147+
crate_ids: &[i32],
148+
parts: &Parts,
149+
) -> AppResult<Json<ListResponse>> {
72150
let pagination = PaginationOptions::builder()
73151
.enable_seek(true)
74152
.enable_pages(false)
75-
.gather(&parts)?;
76-
77-
let (configs, total, next_page) =
78-
list_configs(&mut conn, krate.id, &pagination, &parts).await?;
79-
80-
let github_configs = configs
81-
.into_iter()
82-
.map(|config| json::GitHubConfig {
83-
id: config.id,
84-
krate: krate.name.clone(),
85-
repository_owner: config.repository_owner,
86-
repository_owner_id: config.repository_owner_id,
87-
repository_name: config.repository_name,
88-
workflow_filename: config.workflow_filename,
89-
environment: config.environment,
90-
created_at: config.created_at,
91-
})
92-
.collect();
153+
.gather(parts)?;
154+
155+
let (configs, total, next_page) = list_configs(conn, crate_ids, &pagination, parts).await?;
156+
157+
let github_configs = configs.into_iter().map(to_json_config).collect();
93158

94159
Ok(Json(ListResponse {
95160
github_configs,
96161
meta: ListResponseMeta { total, next_page },
97162
}))
98163
}
99164

165+
fn to_json_config(config: ConfigWithCrateName) -> json::GitHubConfig {
166+
let crate_name = config.crate_name;
167+
let config = config.config;
168+
169+
json::GitHubConfig {
170+
id: config.id,
171+
krate: crate_name,
172+
repository_owner: config.repository_owner,
173+
repository_owner_id: config.repository_owner_id,
174+
repository_name: config.repository_name,
175+
workflow_filename: config.workflow_filename,
176+
environment: config.environment,
177+
created_at: config.created_at,
178+
}
179+
}
180+
181+
#[derive(Debug, HasQuery)]
182+
#[diesel(base_query = trustpub_configs_github::table.inner_join(crates::table))]
183+
#[diesel(check_for_backend(diesel::pg::Pg))]
184+
struct ConfigWithCrateName {
185+
#[diesel(select_expression = crates::name)]
186+
crate_name: String,
187+
#[diesel(embed)]
188+
config: GitHubConfig,
189+
}
190+
100191
async fn list_configs(
101192
conn: &mut diesel_async::AsyncPgConnection,
102-
crate_id: i32,
193+
crate_ids: &[i32],
103194
options: &PaginationOptions,
104195
req: &Parts,
105-
) -> AppResult<(Vec<GitHubConfig>, i64, Option<String>)> {
196+
) -> AppResult<(Vec<ConfigWithCrateName>, i64, Option<String>)> {
106197
use seek::*;
107198

108199
let seek = Seek::Id;
@@ -113,8 +204,8 @@ async fn list_configs(
113204
);
114205

115206
let make_base_query = || {
116-
GitHubConfig::query()
117-
.filter(trustpub_configs_github::crate_id.eq(crate_id))
207+
ConfigWithCrateName::query()
208+
.filter(trustpub_configs_github::crate_id.eq_any(crate_ids))
118209
.into_boxed()
119210
};
120211

@@ -126,7 +217,7 @@ async fn list_configs(
126217
query = query.filter(trustpub_configs_github::id.gt(id));
127218
}
128219

129-
let data: Vec<GitHubConfig> = query.load(conn).await?;
220+
let data = query.load(conn).await?;
130221

131222
let next_page = next_seek_params(&data, options, |last| seek.to_payload(last))?
132223
.map(|p| req.query_with_params(p));
@@ -162,8 +253,8 @@ where
162253
}
163254

164255
mod seek {
256+
use super::ConfigWithCrateName;
165257
use crate::controllers::helpers::pagination::seek;
166-
use crates_io_database::models::trustpub::GitHubConfig;
167258

168259
seek!(
169260
pub enum Seek {
@@ -172,9 +263,11 @@ mod seek {
172263
);
173264

174265
impl Seek {
175-
pub(crate) fn to_payload(&self, record: &GitHubConfig) -> SeekPayload {
266+
pub(crate) fn to_payload(&self, record: &ConfigWithCrateName) -> SeekPayload {
176267
match *self {
177-
Seek::Id => SeekPayload::Id(Id { id: record.id }),
268+
Seek::Id => SeekPayload::Id(Id {
269+
id: record.config.id,
270+
}),
178271
}
179272
}
180273
}

0 commit comments

Comments
 (0)