Rust implementation of OCI Distribution Spec with granular access control
1use axum::{body::Body, http::StatusCode, response::IntoResponse, response::Response};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub enum ErrorCode {
6 #[serde(rename = "BLOB_UNKNOWN")]
7 BlobUnknown,
8
9 #[serde(rename = "BLOB_UPLOAD_INVALID")]
10 BlobUploadInvalid,
11
12 #[serde(rename = "BLOB_UPLOAD_UNKNOWN")]
13 BlobUploadUnknown,
14
15 #[serde(rename = "DIGEST_INVALID")]
16 DigestInvalid,
17
18 #[serde(rename = "MANIFEST_BLOB_UNKNOWN")]
19 ManifestBlobUnknown,
20
21 #[serde(rename = "MANIFEST_INVALID")]
22 ManifestInvalid,
23
24 #[serde(rename = "MANIFEST_UNKNOWN")]
25 ManifestUnknown,
26
27 #[serde(rename = "MANIFEST_UNVERIFIED")]
28 ManifestUnverified,
29
30 #[serde(rename = "NAME_INVALID")]
31 NameInvalid,
32
33 #[serde(rename = "NAME_UNKNOWN")]
34 NameUnknown,
35
36 #[serde(rename = "SIZE_INVALID")]
37 SizeInvalid,
38
39 #[serde(rename = "TAG_INVALID")]
40 TagInvalid,
41
42 #[serde(rename = "UNAUTHORIZED")]
43 Unauthorized,
44
45 #[serde(rename = "DENIED")]
46 Denied,
47
48 #[serde(rename = "UNSUPPORTED")]
49 Unsupported,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct OciError {
54 pub code: ErrorCode,
55 pub message: String,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub detail: Option<String>,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
61pub struct OciErrorResponse {
62 pub errors: Vec<OciError>,
63}
64
65impl OciErrorResponse {
66 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
67 Self {
68 errors: vec![OciError {
69 code,
70 message: message.into(),
71 detail: None,
72 }],
73 }
74 }
75
76 pub fn with_detail(
77 code: ErrorCode,
78 message: impl Into<String>,
79 detail: impl Into<String>,
80 ) -> Self {
81 Self {
82 errors: vec![OciError {
83 code,
84 message: message.into(),
85 detail: Some(detail.into()),
86 }],
87 }
88 }
89
90 pub fn to_response(&self, status: StatusCode) -> Response {
91 let json = serde_json::to_string(self).unwrap_or_else(|_| {
92 r#"{"errors":[{"code":"UNKNOWN","message":"internal error"}]}"#.to_string()
93 });
94
95 Response::builder()
96 .status(status)
97 .header("Content-Type", "application/json")
98 .body(Body::from(json))
99 .unwrap()
100 }
101}
102
103impl IntoResponse for OciErrorResponse {
104 fn into_response(self) -> Response {
105 let status = match self.errors.first() {
106 Some(err) => match err.code {
107 ErrorCode::Unauthorized => StatusCode::UNAUTHORIZED,
108 ErrorCode::Denied => StatusCode::FORBIDDEN,
109 ErrorCode::BlobUnknown
110 | ErrorCode::ManifestUnknown
111 | ErrorCode::NameUnknown
112 | ErrorCode::BlobUploadUnknown => StatusCode::NOT_FOUND,
113 ErrorCode::DigestInvalid
114 | ErrorCode::ManifestInvalid
115 | ErrorCode::NameInvalid
116 | ErrorCode::TagInvalid
117 | ErrorCode::SizeInvalid
118 | ErrorCode::BlobUploadInvalid => StatusCode::BAD_REQUEST,
119 ErrorCode::Unsupported => StatusCode::METHOD_NOT_ALLOWED,
120 ErrorCode::ManifestBlobUnknown | ErrorCode::ManifestUnverified => {
121 StatusCode::BAD_REQUEST
122 }
123 },
124 None => StatusCode::INTERNAL_SERVER_ERROR,
125 };
126
127 self.to_response(status)
128 }
129}