radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

auth.rs (4722B)


      1 #![forbid(unsafe_code)]
      2 
      3 use jsonrpsee::core::server::Extensions;
      4 
      5 use crate::core::publish_proxy::{PublishPrincipal, PublishProxyStore, hash_bearer_token};
      6 
      7 use super::RpcError;
      8 
      9 #[cfg(test)]
     10 pub(crate) const PUBLISH_PROXY_AUTH_MODE: &str = "scoped_bearer_token";
     11 
     12 #[derive(Clone, Debug, PartialEq, Eq)]
     13 pub(crate) enum PublishProxyAuthorization {
     14     Authorized(PublishPrincipal),
     15     Missing,
     16     Invalid,
     17 }
     18 
     19 pub(crate) fn authorize_publish_proxy_request(
     20     authorization_header: Option<&str>,
     21     store: &PublishProxyStore,
     22 ) -> PublishProxyAuthorization {
     23     let Some(authorization_header) = authorization_header else {
     24         return PublishProxyAuthorization::Missing;
     25     };
     26 
     27     let mut parts = authorization_header.split_whitespace();
     28     let scheme = parts.next().unwrap_or_default();
     29     let token = parts.next().unwrap_or_default();
     30 
     31     if !scheme.eq_ignore_ascii_case("bearer") || token.is_empty() || parts.next().is_some() {
     32         return PublishProxyAuthorization::Invalid;
     33     }
     34 
     35     match store.principal_for_token_hash(hash_bearer_token(token).as_str()) {
     36         Ok(Some(principal)) => PublishProxyAuthorization::Authorized(principal),
     37         Ok(None) | Err(_) => PublishProxyAuthorization::Invalid,
     38     }
     39 }
     40 
     41 pub(crate) fn require_publish_principal(
     42     extensions: &Extensions,
     43 ) -> Result<PublishPrincipal, RpcError> {
     44     match extensions
     45         .get::<PublishProxyAuthorization>()
     46         .cloned()
     47         .unwrap_or(PublishProxyAuthorization::Missing)
     48     {
     49         PublishProxyAuthorization::Authorized(principal) => Ok(principal),
     50         PublishProxyAuthorization::Missing => Err(RpcError::Unauthorized(
     51             "publish proxy bearer token required".to_string(),
     52         )),
     53         PublishProxyAuthorization::Invalid => Err(RpcError::Unauthorized(
     54             "invalid publish proxy bearer token".to_string(),
     55         )),
     56     }
     57 }
     58 
     59 #[cfg(test)]
     60 mod tests {
     61     use jsonrpsee::core::server::Extensions;
     62     use radroots_publish_proxy_protocol::PublishRelayPolicy;
     63 
     64     use super::{
     65         PUBLISH_PROXY_AUTH_MODE, PublishProxyAuthorization, authorize_publish_proxy_request,
     66         require_publish_principal,
     67     };
     68     use crate::core::publish_proxy::{
     69         PublishJobVisibility, PublishPrincipalInit, PublishProxyStore, generate_bearer_token,
     70         hash_bearer_token,
     71     };
     72 
     73     fn store_with_token() -> (PublishProxyStore, String) {
     74         let store = PublishProxyStore::memory().expect("store");
     75         let token = generate_bearer_token();
     76         store
     77             .create_principal(PublishPrincipalInit {
     78                 label: "tester".to_owned(),
     79                 token_hash: hash_bearer_token(token.as_str()),
     80                 allowed_pubkeys: vec!["a".repeat(64)],
     81                 allowed_kinds: vec![30_402],
     82                 allowed_relay_policies: vec![PublishRelayPolicy::DaemonDefaultOnly],
     83                 allow_request_relays: false,
     84                 job_visibility: PublishJobVisibility::Own,
     85                 expires_at_unix: None,
     86             })
     87             .expect("principal");
     88         (store, token)
     89     }
     90 
     91     #[test]
     92     fn publish_proxy_auth_accepts_matching_bearer_token() {
     93         let (store, token) = store_with_token();
     94         let header = format!("Bearer {token}");
     95         let auth = authorize_publish_proxy_request(Some(header.as_str()), &store);
     96         assert!(matches!(auth, PublishProxyAuthorization::Authorized(_)));
     97         assert_eq!(PUBLISH_PROXY_AUTH_MODE, "scoped_bearer_token");
     98     }
     99 
    100     #[test]
    101     fn publish_proxy_auth_rejects_missing_and_invalid_headers() {
    102         let (store, _token) = store_with_token();
    103         assert_eq!(
    104             authorize_publish_proxy_request(None, &store),
    105             PublishProxyAuthorization::Missing
    106         );
    107         assert_eq!(
    108             authorize_publish_proxy_request(Some("Basic secret"), &store),
    109             PublishProxyAuthorization::Invalid
    110         );
    111         assert_eq!(
    112             authorize_publish_proxy_request(Some("Bearer wrong"), &store),
    113             PublishProxyAuthorization::Invalid
    114         );
    115     }
    116 
    117     #[test]
    118     fn require_publish_principal_reads_authorized_extensions() {
    119         let (store, token) = store_with_token();
    120         let header = format!("Bearer {token}");
    121         let auth = authorize_publish_proxy_request(Some(header.as_str()), &store);
    122         let mut extensions = Extensions::new();
    123         extensions.insert(auth);
    124         require_publish_principal(&extensions).expect("authorized");
    125     }
    126 
    127     #[test]
    128     fn require_publish_principal_rejects_missing_extensions() {
    129         let err =
    130             require_publish_principal(&Extensions::new()).expect_err("missing auth should fail");
    131         assert!(err.to_string().contains("required"));
    132     }
    133 }