radrootsd

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

auth.rs (5077B)


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