kellnr_settings/
registry.rs1use std::num::NonZeroU64;
2use std::time::Duration;
3
4use provcfg::{ClapArgs, Configurable};
5use serde::{Deserialize, Serialize};
6use utoipa::ToSchema;
7
8use crate::compile_time_config;
9use crate::constants::MAX_SESSION_AGE;
10
11fn default_data_dir() -> String {
12 std::env::var("KELLNR_DATA_DIR")
14 .ok()
15 .or_else(|| compile_time_config::KELLNR_COMPTIME__DATA_DIR.map(String::from))
16 .unwrap_or_default()
17}
18
19#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs, ToSchema)]
20#[serde(default)]
21#[configurable(clap_prefix = "registry")]
22#[allow(clippy::struct_excessive_bools)]
23pub struct Registry {
24 #[arg(short = 'd')]
26 pub data_dir: String,
27
28 #[arg(long = "registry-session-age")]
31 pub session_age_seconds: u64,
32
33 pub cache_size: u64,
35
36 pub cache_max_size: u64,
39
40 pub max_crate_size: u64,
42
43 pub max_db_connections: u32,
45
46 pub auth_required: bool,
48
49 #[configurable(env_list)]
51 #[arg(value_delimiter = ',')]
52 pub required_crate_fields: Vec<String>,
53
54 pub new_crates_restricted: bool,
56
57 pub cookie_signing_key: Option<String>,
61
62 pub allow_ownerless_crates: bool,
64
65 pub token_cache_enabled: bool,
67
68 #[arg(long = "registry-token-cache-ttl")]
70 pub token_cache_ttl_seconds: u64,
71
72 pub token_cache_max_capacity: u64,
74
75 pub token_db_retry_count: u32,
77
78 #[arg(long = "registry-token-db-retry-delay")]
80 pub token_db_retry_delay_ms: u64,
81
82 #[arg(long = "registry-download-timeout")]
84 pub download_timeout_seconds: u64,
85
86 pub download_max_concurrent: usize,
88
89 #[arg(long = "registry-download-counter-flush")]
91 pub download_counter_flush_seconds: u64,
92
93 pub auth_max_failed_attempts: u32,
97
98 #[arg(long = "registry-auth-failed-attempts-window")]
100 pub auth_failed_attempts_window_seconds: u64,
101
102 pub sponsor_links_enabled: bool,
104}
105
106impl Registry {
107 pub fn session_age(&self) -> SessionAge {
108 SessionAge::from_secs(self.session_age_seconds)
109 }
110
111 pub fn token_cache_ttl(&self) -> Duration {
112 Duration::from_secs(self.token_cache_ttl_seconds)
113 }
114
115 pub fn auth_failed_attempts_window(&self) -> Duration {
116 Duration::from_secs(self.auth_failed_attempts_window_seconds)
117 }
118
119 pub fn max_crate_bytes(&self) -> usize {
121 mb_to_bytes(self.max_crate_size)
122 }
123
124 pub fn cache_max_bytes(&self) -> Option<u64> {
127 (self.cache_max_size > 0).then(|| self.cache_max_size.saturating_mul(1_000_000))
128 }
129}
130
131pub(crate) fn mb_to_bytes(mb: impl TryInto<usize>) -> usize {
134 mb.try_into()
135 .unwrap_or(usize::MAX)
136 .saturating_mul(1_000_000)
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141pub enum SessionAge {
142 Never,
144 For(NonZeroU64),
146}
147
148impl SessionAge {
149 pub fn from_secs(secs: u64) -> Self {
151 NonZeroU64::new(secs).map_or(Self::Never, Self::For)
152 }
153
154 pub fn expiry(self) -> Option<Duration> {
158 match self {
159 Self::Never => None,
160 Self::For(secs) => Some(Duration::from_secs(secs.get()).min(MAX_SESSION_AGE)),
161 }
162 }
163
164 pub fn cookie_max_age(self) -> Duration {
167 self.expiry().unwrap_or(MAX_SESSION_AGE)
168 }
169}
170
171impl Default for Registry {
172 fn default() -> Self {
173 Self {
174 data_dir: default_data_dir(),
175 session_age_seconds: 60 * 60 * 8,
176 cache_size: 1000,
177 cache_max_size: 0,
178 max_crate_size: 10,
179 max_db_connections: 0,
180 auth_required: false,
181 required_crate_fields: Vec::new(),
182 new_crates_restricted: false,
183 cookie_signing_key: None,
184 allow_ownerless_crates: false,
185 token_cache_enabled: true,
186 token_cache_ttl_seconds: 1800,
187 token_cache_max_capacity: 10000,
188 token_db_retry_count: 3,
189 token_db_retry_delay_ms: 100,
190 download_timeout_seconds: 60,
191 download_max_concurrent: 20,
192 download_counter_flush_seconds: 30,
193 auth_max_failed_attempts: 10,
194 auth_failed_attempts_window_seconds: 300,
195 sponsor_links_enabled: true,
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 fn registry(session_age_seconds: u64) -> Registry {
205 Registry {
206 session_age_seconds,
207 ..Registry::default()
208 }
209 }
210
211 #[test]
212 fn session_age_of_zero_never_expires() {
213 let age = registry(0).session_age();
214 assert_eq!(age, SessionAge::Never);
215 assert_eq!(age.expiry(), None);
216 assert_eq!(age.cookie_max_age(), MAX_SESSION_AGE);
217 }
218
219 #[test]
220 fn session_age_is_the_configured_age() {
221 let age = registry(60).session_age();
222 assert_eq!(age, SessionAge::For(NonZeroU64::new(60).unwrap()));
223 assert_eq!(age.expiry(), Some(Duration::from_mins(1)));
224 assert_eq!(age.cookie_max_age(), Duration::from_mins(1));
225 }
226
227 #[test]
228 fn session_age_is_bounded() {
229 let age = registry(u64::MAX).session_age();
230 assert_eq!(age.expiry(), Some(MAX_SESSION_AGE));
231 assert_eq!(age.cookie_max_age(), MAX_SESSION_AGE);
232 }
233
234 #[test]
235 fn durations_are_read_in_seconds() {
236 let registry = Registry {
237 token_cache_ttl_seconds: 90,
238 auth_failed_attempts_window_seconds: 300,
239 ..Registry::default()
240 };
241 assert_eq!(registry.token_cache_ttl(), Duration::from_secs(90));
242 assert_eq!(
243 registry.auth_failed_attempts_window(),
244 Duration::from_mins(5)
245 );
246 }
247
248 #[test]
249 fn sizes_are_read_in_mb_and_saturate() {
250 let mut registry = Registry {
251 max_crate_size: 10,
252 cache_max_size: 0,
253 ..Registry::default()
254 };
255 assert_eq!(registry.max_crate_bytes(), 10_000_000);
256 assert_eq!(registry.cache_max_bytes(), None);
257
258 registry.max_crate_size = u64::MAX;
259 registry.cache_max_size = u64::MAX;
260 assert_eq!(registry.max_crate_bytes(), usize::MAX);
261 assert_eq!(registry.cache_max_bytes(), Some(u64::MAX));
262
263 registry.cache_max_size = 2;
264 assert_eq!(registry.cache_max_bytes(), Some(2_000_000));
265 }
266}