kurrentdb 1.1.0

Official KurrentDB gRPC client
Documentation
use crate::options::CommonOperationOptions;
use crate::{Authentication, ClientSettings, Credentials, NodePreference};
use base64::Engine;
use std::borrow::Cow;

pub(crate) fn build_request_metadata(
    settings: &ClientSettings,
    options: &CommonOperationOptions,
) -> tonic::metadata::MetadataMap
where
{
    use tonic::metadata::MetadataValue;

    let mut metadata = tonic::metadata::MetadataMap::new();
    let authentication: Option<Cow<'_, Authentication>> = options
        .authentication
        .as_ref()
        .map(Cow::Borrowed)
        .or_else(|| {
            settings
                .default_authenticated_user()
                .as_ref()
                .map(|c| Cow::Owned(Authentication::Basic(c.clone())))
        });

    if let Some(header_value) = authentication
        .as_deref()
        .and_then(build_authorization_header)
    {
        metadata.insert("authorization", header_value);
    }

    if options.requires_leader || settings.node_preference() == NodePreference::Leader {
        let header_value = MetadataValue::try_from("true").expect("valid metadata header value");
        metadata.insert("requires-leader", header_value);
    }

    if let Some(conn_name) = settings.connection_name.as_ref() {
        let header_value =
            MetadataValue::try_from(conn_name.as_str()).expect("valid metadata header value");
        metadata.insert("connection-name", header_value);
    }

    metadata
}

fn build_authorization_header(
    auth: &Authentication,
) -> Option<tonic::metadata::MetadataValue<tonic::metadata::Ascii>> {
    use tonic::metadata::MetadataValue;

    let header = match auth {
        Authentication::Basic(Credentials { login, password }) => {
            let login = String::from_utf8_lossy(login);
            let password = String::from_utf8_lossy(password);
            let encoded =
                base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", login, password));
            format!("Basic {}", encoded)
        }
        Authentication::Bearer(token) => {
            let token = String::from_utf8_lossy(token);
            format!("Bearer {}", token)
        }
    };

    match MetadataValue::try_from(header.as_str()) {
        Ok(value) => Some(value),
        Err(_) => {
            tracing::warn!(
                auth_kind = auth.kind(),
                "authentication value contains characters that are not valid in a gRPC metadata header; the Authorization header will be omitted"
            );
            None
        }
    }
}

#[cfg(test)]
mod auth_tests {
    use super::*;
    use crate::AppendToStreamOptions;
    use crate::options::Options;

    fn settings_from(connection_string: &str) -> ClientSettings {
        connection_string
            .parse::<ClientSettings>()
            .expect("valid connection string")
    }

    #[test]
    fn basic_authentication_produces_base64_basic_header() {
        let auth = Authentication::basic("admin", "changeit");
        let header = build_authorization_header(&auth).expect("ASCII header");
        assert_eq!(header.to_str().unwrap(), "Basic YWRtaW46Y2hhbmdlaXQ=");
    }

    #[test]
    fn bearer_authentication_produces_bearer_header_verbatim() {
        let auth = Authentication::bearer("abc.def.ghi");
        let header = build_authorization_header(&auth).expect("ASCII header");
        assert_eq!(header.to_str().unwrap(), "Bearer abc.def.ghi");
    }

    #[test]
    fn basic_authentication_with_special_chars_encodes_correctly() {
        let auth = Authentication::basic("user@example.com", "p@ss:word");
        let header = build_authorization_header(&auth).expect("ASCII header");
        assert_eq!(
            header.to_str().unwrap(),
            "Basic dXNlckBleGFtcGxlLmNvbTpwQHNzOndvcmQ="
        );
    }

    #[test]
    fn build_request_metadata_skips_bearer_token_with_invalid_chars() {
        let settings = settings_from("esdb://localhost:2113?tls=false");
        let options =
            AppendToStreamOptions::default().authenticated(Authentication::bearer("token\nleak"));
        let metadata = build_request_metadata(&settings, options.common_operation_options());
        assert!(metadata.get("authorization").is_none());
    }

    #[test]
    fn no_auth_anywhere_produces_no_authorization_header() {
        let settings = settings_from("esdb://localhost:2113?tls=false");
        let options = AppendToStreamOptions::default();
        let metadata = build_request_metadata(&settings, options.common_operation_options());

        assert!(metadata.get("authorization").is_none());
    }

    #[test]
    fn default_user_from_connection_string_falls_through_as_basic() {
        let settings = settings_from("esdb://admin:changeit@localhost:2113?tls=false");
        let options = AppendToStreamOptions::default();
        let metadata = build_request_metadata(&settings, options.common_operation_options());

        assert_eq!(
            metadata.get("authorization").unwrap().to_str().unwrap(),
            "Basic YWRtaW46Y2hhbmdlaXQ="
        );
    }

    #[test]
    fn per_call_bearer_overrides_default_user() {
        let settings = settings_from("esdb://admin:changeit@localhost:2113?tls=false");
        let options =
            AppendToStreamOptions::default().authenticated(Authentication::bearer("call-token"));
        let metadata = build_request_metadata(&settings, options.common_operation_options());

        assert_eq!(
            metadata.get("authorization").unwrap().to_str().unwrap(),
            "Bearer call-token"
        );
    }

    #[test]
    fn authenticated_builder_accepts_credentials_directly() {
        let settings = settings_from("esdb://localhost:2113?tls=false");
        let options =
            AppendToStreamOptions::default().authenticated(Credentials::new("alice", "secret"));
        let metadata = build_request_metadata(&settings, options.common_operation_options());

        assert_eq!(
            metadata.get("authorization").unwrap().to_str().unwrap(),
            "Basic YWxpY2U6c2VjcmV0"
        );
    }
}