Rust implementation of OCI Distribution Spec with granular access control
1// | ID | Method | API Endpoint | Success | Failure |
2// | ------ | -------------- | ------------------------------------------------------------ | ----------- | ----------------- |
3// | end-8a | `GET` | `/v2/<name>/tags/list` | `200` | `404` |
4// | end-8b | `GET` | `/v2/<name>/tags/list?n=<integer>&last=<integer>` | `200` | `404` |
5
6use axum::body::Body;
7use axum::http::{HeaderMap, StatusCode};
8use axum::response::Response;
9use serde::Deserialize;
10use std::sync::Arc;
11
12use crate::{auth, permissions, response, state, storage};
13use axum::extract::{Path, Query, State};
14
15// end-8a GET /v2/:name/tags/list
16// end-8b GET /v2/:name/tags/list?n=<integer>&last=<integer>
17#[derive(Deserialize)]
18pub(crate) struct TagsQuery {
19 pub n: Option<usize>,
20 pub last: Option<String>,
21}
22
23fn paginate_tags(tags: Vec<String>, n: Option<usize>, last: Option<String>) -> Vec<String> {
24 let mut result = tags;
25
26 // Filter tags after 'last' cursor
27 if let Some(last_tag) = last {
28 result = result
29 .into_iter()
30 .skip_while(|tag| tag <= &last_tag)
31 .collect();
32 }
33
34 // Limit to 'n' results
35 if let Some(limit) = n {
36 result.truncate(limit);
37 }
38
39 result
40}
41
42pub(crate) async fn get_tags_list(
43 State(state): State<Arc<state::App>>,
44 Path((org, repo)): Path<(String, String)>,
45 Query(params): Query<TagsQuery>,
46 headers: HeaderMap,
47) -> Response<Body> {
48 let host = &state.args.host;
49 let repository = format!("{}/{}", org, repo);
50
51 // Check permission (Pull for tag listing)
52 match auth::check_permission(
53 &state,
54 &headers,
55 &repository,
56 None,
57 permissions::Action::Pull,
58 )
59 .await
60 {
61 Ok(_) => {}
62 Err(_) => {
63 return if auth::authenticate_user(&state, &headers).await.is_ok() {
64 response::forbidden()
65 } else {
66 response::unauthorized(host)
67 };
68 }
69 }
70
71 // Get all tags from storage
72 match storage::list_tags(&org, &repo) {
73 Ok(all_tags) => {
74 // Apply pagination
75 let paginated_tags = paginate_tags(all_tags, params.n, params.last);
76
77 // Build response JSON
78 let response_body = serde_json::json!({
79 "name": format!("{}/{}", org, repo),
80 "tags": paginated_tags
81 });
82
83 Response::builder()
84 .status(StatusCode::OK)
85 .header("Content-Type", "application/json")
86 .body(Body::from(response_body.to_string()))
87 .unwrap()
88 }
89 Err(e) => {
90 log::error!("Failed to list tags for {}/{}: {}", org, repo, e);
91
92 // Return empty list if directory doesn't exist (valid case)
93 let response_body = serde_json::json!({
94 "name": format!("{}/{}", org, repo),
95 "tags": []
96 });
97
98 Response::builder()
99 .status(StatusCode::OK)
100 .header("Content-Type", "application/json")
101 .body(Body::from(response_body.to_string()))
102 .unwrap()
103 }
104 }
105}