Skip to main content

pray_core/
push_auth.rs

1use crate::ssh_publishers::authorize_ssh_push;
2use crate::{PrayError, PrayResult};
3use std::path::Path;
4
5pub fn authorize_distribution_push(
6    root: &Path,
7    bind_host: &str,
8    allow_open_push: bool,
9    stdio_mode: bool,
10    authorization: Option<&str>,
11) -> PrayResult<()> {
12    if let Some(token) = publish_bearer_token(authorization) {
13        return authorize_publish_token(root, &token);
14    }
15
16    let stdio_mode = stdio_mode || std::env::var_os("PRAY_SERVE_STDIO").is_some();
17    if stdio_mode {
18        return authorize_ssh_push(root);
19    }
20
21    match authorize_ssh_push(root) {
22        Ok(()) => {
23            if publishers_configured(root)? {
24                return Ok(());
25            }
26        }
27        Err(error) => return Err(error),
28    }
29
30    if allow_open_push || is_loopback_bind_host(bind_host) {
31        return Ok(());
32    }
33
34    Err(PrayError::Resolution(
35        "HTTP push requires a publish token (Authorization: Bearer), configured ssh publishers, loopback bind, or --allow-open-push"
36            .to_string(),
37    ))
38}
39
40fn publish_bearer_token(authorization: Option<&str>) -> Option<String> {
41    #[cfg(feature = "auth")]
42    {
43        crate::auth_store::bearer_token_from_authorization(authorization)
44    }
45    #[cfg(not(feature = "auth"))]
46    {
47        let _ = authorization;
48        None
49    }
50}
51
52fn authorize_publish_token(root: &Path, token: &str) -> PrayResult<()> {
53    #[cfg(feature = "auth")]
54    {
55        let store = crate::auth_store::RegistryAuthStore::open(root)?;
56        match store.resolve_publish_token(token)? {
57            Some(_) => Ok(()),
58            None => Err(PrayError::Resolution(
59                "invalid or unknown publish token".to_string(),
60            )),
61        }
62    }
63    #[cfg(not(feature = "auth"))]
64    {
65        let _ = (root, token);
66        Err(PrayError::Unsupported(
67            "publish tokens require the auth feature".to_string(),
68        ))
69    }
70}
71
72fn publishers_configured(root: &Path) -> PrayResult<bool> {
73    match crate::ssh_publishers::read_ssh_publishers(root)? {
74        Some(config) => Ok(!config.publishers.is_empty()),
75        None => Ok(false),
76    }
77}
78
79pub fn is_loopback_bind_host(host: &str) -> bool {
80    matches!(host, "127.0.0.1" | "localhost" | "::1" | "0:0:0:0:0:0:0:1")
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use std::fs;
87
88    #[test]
89    fn loopback_allows_open_push_without_publishers() {
90        let root =
91            std::env::temp_dir().join(format!("pray-push-auth-loopback-{}", std::process::id()));
92        let _ = fs::remove_dir_all(&root);
93        fs::create_dir_all(&root).expect("temp root");
94        authorize_distribution_push(&root, "127.0.0.1", false, false, None)
95            .expect("loopback open push");
96        let _ = fs::remove_dir_all(&root);
97    }
98
99    #[test]
100    fn non_loopback_requires_flag_without_publishers() {
101        let root =
102            std::env::temp_dir().join(format!("pray-push-auth-public-{}", std::process::id()));
103        let _ = fs::remove_dir_all(&root);
104        fs::create_dir_all(&root).expect("temp root");
105        let error = authorize_distribution_push(&root, "0.0.0.0", false, false, None)
106            .expect_err("public bind");
107        assert!(error.to_string().contains("--allow-open-push"));
108        authorize_distribution_push(&root, "0.0.0.0", true, false, None).expect("flag allows");
109        let _ = fs::remove_dir_all(&root);
110    }
111}