Skip to content

Commit 58c528b

Browse files
authored
trustpub: Implement GET /api/v1/trusted_publishing/gitlab_configs API endpoint (#12292)
1 parent 18e9cea commit 58c528b

17 files changed

+951
-0
lines changed

src/controllers/trustpub/gitlab_configs/json.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,18 @@ pub struct CreateResponse {
1818
#[derive(Debug, Serialize, utoipa::ToSchema)]
1919
pub struct ListResponse {
2020
pub gitlab_configs: Vec<GitLabConfig>,
21+
22+
#[schema(inline)]
23+
pub meta: ListResponseMeta,
24+
}
25+
26+
#[derive(Debug, Serialize, utoipa::ToSchema)]
27+
pub struct ListResponseMeta {
28+
/// The total number of GitLab configs belonging to the crate.
29+
#[schema(example = 42)]
30+
pub total: i64,
31+
32+
/// Query string to the next page of results, if any.
33+
#[schema(example = "?seek=abc123")]
34+
pub next_page: Option<String>,
2135
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
use crate::app::AppState;
2+
use crate::auth::AuthCheck;
3+
use crate::controllers::helpers::pagination::{
4+
Page, PaginationOptions, PaginationQueryParams, encode_seek,
5+
};
6+
use crate::controllers::krate::load_crate;
7+
use crate::controllers::trustpub::gitlab_configs::json::{self, ListResponse, ListResponseMeta};
8+
use crate::util::RequestUtils;
9+
use crate::util::errors::{AppResult, bad_request};
10+
use axum::Json;
11+
use axum::extract::{FromRequestParts, Query};
12+
use crates_io_database::models::OwnerKind;
13+
use crates_io_database::models::token::EndpointScope;
14+
use crates_io_database::models::trustpub::GitLabConfig;
15+
use crates_io_database::schema::{crate_owners, trustpub_configs_gitlab};
16+
use diesel::dsl::{exists, select};
17+
use diesel::prelude::*;
18+
use diesel_async::RunQueryDsl;
19+
use http::request::Parts;
20+
use indexmap::IndexMap;
21+
use serde::Deserialize;
22+
23+
#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
24+
#[from_request(via(Query))]
25+
#[into_params(parameter_in = Query)]
26+
pub struct ListQueryParams {
27+
/// Name of the crate to list Trusted Publishing configurations for.
28+
#[serde(rename = "crate")]
29+
pub krate: String,
30+
}
31+
32+
/// List Trusted Publishing configurations for GitLab CI/CD.
33+
#[utoipa::path(
34+
get,
35+
path = "/api/v1/trusted_publishing/gitlab_configs",
36+
params(ListQueryParams, PaginationQueryParams),
37+
security(("cookie" = []), ("api_token" = [])),
38+
tag = "trusted_publishing",
39+
responses((status = 200, description = "Successful Response", body = inline(ListResponse))),
40+
)]
41+
pub async fn list_trustpub_gitlab_configs(
42+
state: AppState,
43+
params: ListQueryParams,
44+
parts: Parts,
45+
) -> AppResult<Json<ListResponse>> {
46+
let mut conn = state.db_read().await?;
47+
48+
let auth = AuthCheck::default()
49+
.with_endpoint_scope(EndpointScope::TrustedPublishing)
50+
.for_crate(&params.krate)
51+
.check(&parts, &mut conn)
52+
.await?;
53+
let auth_user = auth.user();
54+
55+
let krate = load_crate(&mut conn, &params.krate).await?;
56+
57+
// Check if the authenticated user is an owner of the crate
58+
let is_owner = select(exists(
59+
crate_owners::table
60+
.filter(crate_owners::crate_id.eq(krate.id))
61+
.filter(crate_owners::deleted.eq(false))
62+
.filter(crate_owners::owner_kind.eq(OwnerKind::User))
63+
.filter(crate_owners::owner_id.eq(auth_user.id)),
64+
))
65+
.get_result::<bool>(&mut conn)
66+
.await?;
67+
68+
if !is_owner {
69+
return Err(bad_request("You are not an owner of this crate"));
70+
}
71+
72+
let pagination = PaginationOptions::builder()
73+
.enable_seek(true)
74+
.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 gitlab_configs = configs
81+
.into_iter()
82+
.map(|config| json::GitLabConfig {
83+
id: config.id,
84+
krate: krate.name.clone(),
85+
namespace: config.namespace,
86+
namespace_id: config.namespace_id,
87+
project: config.project,
88+
workflow_filepath: config.workflow_filepath,
89+
environment: config.environment,
90+
created_at: config.created_at,
91+
})
92+
.collect();
93+
94+
Ok(Json(ListResponse {
95+
gitlab_configs,
96+
meta: ListResponseMeta { total, next_page },
97+
}))
98+
}
99+
100+
async fn list_configs(
101+
conn: &mut diesel_async::AsyncPgConnection,
102+
crate_id: i32,
103+
options: &PaginationOptions,
104+
req: &Parts,
105+
) -> AppResult<(Vec<GitLabConfig>, i64, Option<String>)> {
106+
use seek::*;
107+
108+
let seek = Seek::Id;
109+
110+
assert!(
111+
!matches!(&options.page, Page::Numeric(_)),
112+
"?page= is not supported"
113+
);
114+
115+
let make_base_query = || {
116+
GitLabConfig::query()
117+
.filter(trustpub_configs_gitlab::crate_id.eq(crate_id))
118+
.into_boxed()
119+
};
120+
121+
let mut query = make_base_query();
122+
query = query.limit(options.per_page);
123+
query = query.order(trustpub_configs_gitlab::id.asc());
124+
125+
if let Some(SeekPayload::Id(Id { id })) = seek.after(&options.page)? {
126+
query = query.filter(trustpub_configs_gitlab::id.gt(id));
127+
}
128+
129+
let data: Vec<GitLabConfig> = query.load(conn).await?;
130+
131+
let next_page = next_seek_params(&data, options, |last| seek.to_payload(last))?
132+
.map(|p| req.query_with_params(p));
133+
134+
// Avoid the count query if we're on the first page and got fewer results than requested
135+
let total =
136+
if matches!(options.page, Page::Unspecified) && data.len() < options.per_page as usize {
137+
data.len() as i64
138+
} else {
139+
make_base_query().count().get_result(conn).await?
140+
};
141+
142+
Ok((data, total, next_page))
143+
}
144+
145+
fn next_seek_params<T, S, F>(
146+
records: &[T],
147+
options: &PaginationOptions,
148+
f: F,
149+
) -> AppResult<Option<IndexMap<String, String>>>
150+
where
151+
F: Fn(&T) -> S,
152+
S: serde::Serialize,
153+
{
154+
if records.len() < options.per_page as usize {
155+
return Ok(None);
156+
}
157+
158+
let seek = f(records.last().unwrap());
159+
let mut opts = IndexMap::new();
160+
opts.insert("seek".into(), encode_seek(seek)?);
161+
Ok(Some(opts))
162+
}
163+
164+
mod seek {
165+
use crate::controllers::helpers::pagination::seek;
166+
use crates_io_database::models::trustpub::GitLabConfig;
167+
168+
seek!(
169+
pub enum Seek {
170+
Id { id: i32 },
171+
}
172+
);
173+
174+
impl Seek {
175+
pub(crate) fn to_payload(&self, record: &GitLabConfig) -> SeekPayload {
176+
match *self {
177+
Seek::Id => SeekPayload::Id(Id { id: record.id }),
178+
}
179+
}
180+
}
181+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod create;
22
pub mod delete;
33
pub mod json;
4+
pub mod list;

src/router.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> {
101101
.routes(routes!(
102102
trustpub::gitlab_configs::create::create_trustpub_gitlab_config,
103103
trustpub::gitlab_configs::delete::delete_trustpub_gitlab_config,
104+
trustpub::gitlab_configs::list::list_trustpub_gitlab_configs,
104105
))
105106
.split_for_parts();
106107

0 commit comments

Comments
 (0)