1#![doc = include_str!("../examples/file-provider.rs")]
41#![allow(clippy::print_stderr)]
44#![allow(clippy::print_stdout)]
45
46use serde::{Deserialize, Serialize};
47use std::{fmt::Display, io};
48use time::OffsetDateTime;
49
50mod error;
51mod secret;
52mod stdio;
53
54pub use error::Error;
55pub use secret::Secret;
56use stdio::stdin_stdout_to_console;
57
58#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
60pub struct CredentialHello {
61 pub v: Vec<u32>,
63}
64
65pub struct UnsupportedCredential;
67impl Credential for UnsupportedCredential {
68 fn perform(
69 &self,
70 _registry: &RegistryInfo<'_>,
71 _action: &Action<'_>,
72 _args: &[&str],
73 ) -> Result<CredentialResponse, Error> {
74 Err(Error::UrlNotSupported)
75 }
76}
77
78#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
80#[serde(rename_all = "kebab-case")]
81pub struct CredentialRequest<'a> {
82 pub v: u32,
84 #[serde(borrow)]
85 pub registry: RegistryInfo<'a>,
86 #[serde(borrow, flatten)]
87 pub action: Action<'a>,
88 #[serde(skip_serializing_if = "Vec::is_empty", default)]
90 pub args: Vec<&'a str>,
91}
92
93#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
94#[serde(rename_all = "kebab-case")]
95pub struct RegistryInfo<'a> {
96 pub index_url: &'a str,
98 #[serde(skip_serializing_if = "Option::is_none")]
101 pub name: Option<&'a str>,
102 #[serde(skip_serializing_if = "Vec::is_empty", default)]
104 pub headers: Vec<String>,
105}
106
107#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
108#[non_exhaustive]
109#[serde(tag = "kind", rename_all = "kebab-case")]
110pub enum Action<'a> {
111 #[serde(borrow)]
112 Get(Operation<'a>),
113 Login(LoginOptions<'a>),
114 Logout,
115 #[serde(other)]
116 Unknown,
117}
118
119impl<'a> Display for Action<'a> {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {
122 Action::Get(_) => f.write_str("get"),
123 Action::Login(_) => f.write_str("login"),
124 Action::Logout => f.write_str("logout"),
125 Action::Unknown => f.write_str("<unknown>"),
126 }
127 }
128}
129
130#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
131#[serde(rename_all = "kebab-case")]
132pub struct LoginOptions<'a> {
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub token: Option<Secret<&'a str>>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub login_url: Option<&'a str>,
139}
140
141#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
143#[non_exhaustive]
144#[serde(tag = "operation", rename_all = "kebab-case")]
145pub enum Operation<'a> {
146 Read,
148 Publish {
150 name: &'a str,
152 vers: &'a str,
154 cksum: &'a str,
156 },
157 Yank {
159 name: &'a str,
161 vers: &'a str,
163 },
164 Unyank {
166 name: &'a str,
168 vers: &'a str,
170 },
171 Owners {
173 name: &'a str,
175 },
176 #[serde(other)]
177 Unknown,
178}
179
180#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
182#[serde(tag = "kind", rename_all = "kebab-case")]
183#[non_exhaustive]
184pub enum CredentialResponse {
185 Get {
186 token: Secret<String>,
187 #[serde(flatten)]
188 cache: CacheControl,
189 operation_independent: bool,
190 },
191 Login,
192 Logout,
193 #[serde(other)]
194 Unknown,
195}
196
197#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
198#[serde(tag = "cache", rename_all = "kebab-case")]
199#[non_exhaustive]
200pub enum CacheControl {
201 Never,
203 Expires {
205 #[serde(with = "time::serde::timestamp")]
206 expiration: OffsetDateTime,
207 },
208 Session,
210 #[serde(other)]
211 Unknown,
212}
213
214pub const PROTOCOL_VERSION_1: u32 = 1;
222pub trait Credential {
223 fn perform(
225 &self,
226 registry: &RegistryInfo<'_>,
227 action: &Action<'_>,
228 args: &[&str],
229 ) -> Result<CredentialResponse, Error>;
230}
231
232pub fn main(credential: impl Credential) {
234 let result = doit(credential).map_err(|e| Error::Other(e));
235 if result.is_err() {
236 serde_json::to_writer(std::io::stdout(), &result)
237 .expect("failed to serialize credential provider error");
238 println!();
239 }
240}
241
242fn doit(
243 credential: impl Credential,
244) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
245 let hello = CredentialHello {
246 v: vec![PROTOCOL_VERSION_1],
247 };
248 serde_json::to_writer(std::io::stdout(), &hello)?;
249 println!();
250
251 loop {
252 let mut buffer = String::new();
253 let len = std::io::stdin().read_line(&mut buffer)?;
254 if len == 0 {
255 return Ok(());
256 }
257 let request = deserialize_request(&buffer)?;
258 let response = stdin_stdout_to_console(|| {
259 credential.perform(&request.registry, &request.action, &request.args)
260 })?;
261
262 serde_json::to_writer(std::io::stdout(), &response)?;
263 println!();
264 }
265}
266
267fn deserialize_request(
269 value: &str,
270) -> Result<CredentialRequest<'_>, Box<dyn std::error::Error + Send + Sync>> {
271 let request: CredentialRequest<'_> = serde_json::from_str(&value)?;
272 if request.v != PROTOCOL_VERSION_1 {
273 return Err(format!("unsupported protocol version {}", request.v).into());
274 }
275 Ok(request)
276}
277
278pub fn read_line() -> Result<String, io::Error> {
280 let mut buf = String::new();
281 io::stdin().read_line(&mut buf)?;
282 Ok(buf.trim().to_string())
283}
284
285pub fn read_token(
287 login_options: &LoginOptions<'_>,
288 registry: &RegistryInfo<'_>,
289) -> Result<Secret<String>, Error> {
290 if let Some(token) = &login_options.token {
291 return Ok(token.to_owned());
292 }
293
294 if let Some(url) = login_options.login_url {
295 eprintln!("please paste the token found on {url} below");
296 } else if let Some(name) = registry.name {
297 eprintln!("please paste the token for {name} below");
298 } else {
299 eprintln!("please paste the token for {} below", registry.index_url);
300 }
301
302 Ok(Secret::from(read_line().map_err(Box::new)?))
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn unsupported_version() {
311 let msg = r#"{"v":999, "registry": {"index-url":""}, "args":[], "kind": "unexpected"}"#;
314 assert_eq!(
315 "unsupported protocol version 999",
316 deserialize_request(msg).unwrap_err().to_string()
317 );
318 }
319
320 #[test]
321 fn cache_control() {
322 let cc = CacheControl::Expires {
323 expiration: OffsetDateTime::from_unix_timestamp(1693928537).unwrap(),
324 };
325 let json = serde_json::to_string(&cc).unwrap();
326 assert_eq!(json, r#"{"cache":"expires","expiration":1693928537}"#);
327
328 let cc = CacheControl::Session;
329 let json = serde_json::to_string(&cc).unwrap();
330 assert_eq!(json, r#"{"cache":"session"}"#);
331
332 let cc: CacheControl = serde_json::from_str(r#"{"cache":"unknown-kind"}"#).unwrap();
333 assert_eq!(cc, CacheControl::Unknown);
334
335 assert_eq!(
336 "missing field `expiration`",
337 serde_json::from_str::<CacheControl>(r#"{"cache":"expires"}"#)
338 .unwrap_err()
339 .to_string()
340 );
341 }
342
343 #[test]
344 fn credential_response() {
345 let cr = CredentialResponse::Get {
346 cache: CacheControl::Never,
347 operation_independent: true,
348 token: Secret::from("value".to_string()),
349 };
350 let json = serde_json::to_string(&cr).unwrap();
351 assert_eq!(
352 json,
353 r#"{"kind":"get","token":"value","cache":"never","operation_independent":true}"#
354 );
355
356 let cr = CredentialResponse::Login;
357 let json = serde_json::to_string(&cr).unwrap();
358 assert_eq!(json, r#"{"kind":"login"}"#);
359
360 let cr: CredentialResponse =
361 serde_json::from_str(r#"{"kind":"unknown-kind","extra-data":true}"#).unwrap();
362 assert_eq!(cr, CredentialResponse::Unknown);
363
364 let cr: CredentialResponse =
365 serde_json::from_str(r#"{"kind":"login","extra-data":true}"#).unwrap();
366 assert_eq!(cr, CredentialResponse::Login);
367
368 let cr: CredentialResponse = serde_json::from_str(r#"{"kind":"get","token":"value","cache":"never","operation_independent":true,"extra-field-ignored":123}"#).unwrap();
369 assert_eq!(
370 cr,
371 CredentialResponse::Get {
372 cache: CacheControl::Never,
373 operation_independent: true,
374 token: Secret::from("value".to_string())
375 }
376 );
377 }
378
379 #[test]
380 fn credential_request() {
381 let get_oweners = CredentialRequest {
382 v: PROTOCOL_VERSION_1,
383 args: vec![],
384 registry: RegistryInfo {
385 index_url: "url",
386 name: None,
387 headers: vec![],
388 },
389 action: Action::Get(Operation::Owners { name: "pkg" }),
390 };
391
392 let json = serde_json::to_string(&get_oweners).unwrap();
393 assert_eq!(
394 json,
395 r#"{"v":1,"registry":{"index-url":"url"},"kind":"get","operation":"owners","name":"pkg"}"#
396 );
397
398 let cr: CredentialRequest<'_> =
399 serde_json::from_str(r#"{"extra-1":true,"v":1,"registry":{"index-url":"url","extra-2":true},"kind":"get","operation":"owners","name":"pkg","args":[]}"#).unwrap();
400 assert_eq!(cr, get_oweners);
401 }
402
403 #[test]
404 fn credential_request_logout() {
405 let unknown = CredentialRequest {
406 v: PROTOCOL_VERSION_1,
407 args: vec![],
408 registry: RegistryInfo {
409 index_url: "url",
410 name: None,
411 headers: vec![],
412 },
413 action: Action::Logout,
414 };
415
416 let cr: CredentialRequest<'_> = serde_json::from_str(
417 r#"{"v":1,"registry":{"index-url":"url"},"kind":"logout","extra-1":true,"args":[]}"#,
418 )
419 .unwrap();
420 assert_eq!(cr, unknown);
421 }
422
423 #[test]
424 fn credential_request_unknown() {
425 let unknown = CredentialRequest {
426 v: PROTOCOL_VERSION_1,
427 args: vec![],
428 registry: RegistryInfo {
429 index_url: "",
430 name: None,
431 headers: vec![],
432 },
433 action: Action::Unknown,
434 };
435
436 let cr: CredentialRequest<'_> = serde_json::from_str(
437 r#"{"v":1,"registry":{"index-url":""},"kind":"unexpected-1","extra-1":true,"args":[]}"#,
438 )
439 .unwrap();
440 assert_eq!(cr, unknown);
441 }
442}