alkcall 0.8.0

Call + channels RPC: structured JSON operations, streaming subscriptions, service discovery, and N-channel multiplexing over one transport stream
Documentation
//! Authentication primitives: `AuthContext`, `Identity`, `AuthToken`,
//! `IdentityProvider`.
//!
//! See `docs/architecture/` for the full specification. The trait-based
//! `IdentityProvider` is the integration point — the assembly layer supplies
//! the impl (config-backed, vault-backed, or persistence-adapter-backed) and
//! the call protocol resolves identity per-request through it.

use std::collections::HashMap;
use std::net::SocketAddr;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Identity {
    pub id: String,
    pub scopes: Vec<String>,
    pub resources: HashMap<String, Vec<String>>,
}

#[derive(Debug, Clone)]
pub struct AuthToken {
    pub raw: Vec<u8>,
}

#[derive(Clone)]
pub struct AuthContext {
    pub identity: Option<Identity>,
    pub alpn: Vec<u8>,
    pub remote_addr: Option<SocketAddr>,
    pub tls_client_fingerprint: Option<String>,
}

impl AuthContext {
    /// Construct an `AuthContext` with no identity, no fingerprint, and no
    /// remote address — only the ALPN is set. For POCs, tests, and handlers
    /// that don't require auth.
    pub fn anonymous(alpn: impl Into<Vec<u8>>) -> Self {
        Self {
            identity: None,
            alpn: alpn.into(),
            remote_addr: None,
            tls_client_fingerprint: None,
        }
    }
}

pub trait IdentityProvider: Send + Sync + 'static {
    fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity>;
    fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity>;
}

/// An [`IdentityProvider`] that resolves nothing — every lookup
/// returns `None`. The identity-less posture for consumers that serve
/// ops with no ACL restrictions (or gate by other means), and the
/// default for [`crate::channels::client::ServingConfig`]. Downstream
/// crates supply a real impl (config-backed, vault-backed) to resolve
/// tokens or fingerprints.
pub struct NoopIdentityProvider;

impl IdentityProvider for NoopIdentityProvider {
    fn resolve_from_fingerprint(&self, _: &str) -> Option<Identity> {
        None
    }
    fn resolve_from_token(&self, _: &AuthToken) -> Option<Identity> {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn identity_fields_and_equality() {
        let mut resources = HashMap::new();
        resources.insert("service".to_string(), vec!["gitea".to_string()]);
        let id = Identity {
            id: "SHA256:abc123".to_string(),
            scopes: vec!["relay:connect".to_string()],
            resources,
        };
        let id2 = id.clone();
        assert_eq!(id, id2);
        assert_eq!(id.id, "SHA256:abc123");
    }

    #[test]
    fn auth_token_is_clone() {
        let token = AuthToken {
            raw: b"alk_test".to_vec(),
        };
        let cloned = token.clone();
        assert_eq!(token.raw, cloned.raw);
    }

    #[test]
    fn auth_context_is_clone() {
        let ctx = AuthContext {
            identity: None,
            alpn: b"alk/test".to_vec(),
            remote_addr: None,
            tls_client_fingerprint: None,
        };
        let cloned = ctx.clone();
        assert_eq!(cloned.alpn, b"alk/test");
        assert!(cloned.identity.is_none());
    }

    #[test]
    fn auth_context_anonymous_sets_alpn_only() {
        let ctx = AuthContext::anonymous(b"alk/test");
        assert_eq!(ctx.alpn, b"alk/test");
        assert!(ctx.identity.is_none());
        assert!(ctx.remote_addr.is_none());
        assert!(ctx.tls_client_fingerprint.is_none());
    }
}