use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
use crate::ModuleId;
use crate::transport::{CredentialsToken, OcpiError, TokenRole};
use crate::types::PartyRef;
use crate::{InterfaceRole, VersionNumber};
#[derive(Clone, Debug, PartialEq)]
pub struct AuthenticatedPeer {
pub peer_id: String,
pub role: TokenRole,
pub parties: Vec<PartyRef>,
pub version: VersionNumber,
}
impl AuthenticatedPeer {
#[must_use]
pub fn owns(&self, party: &PartyRef) -> bool {
self.parties.iter().any(|p| p == party)
}
pub fn check_scope(&self, module: &ModuleId) -> Result<(), OcpiError> {
if self.role.may_access(module) { Ok(()) } else { Err(OcpiError::TokenAOutOfScope) }
}
pub fn check_ownership(&self, owner: &PartyRef) -> Result<(), OcpiError> {
if self.owns(owner) {
return Ok(());
}
Err(OcpiError::NotFound(format!("{owner} is not a party of the authenticated platform")))
}
}
pub trait TokenStore: Send + Sync + 'static {
fn resolve(&self, token: &CredentialsToken) -> Option<AuthenticatedPeer>;
}
impl<T: TokenStore> TokenStore for Arc<T> {
fn resolve(&self, token: &CredentialsToken) -> Option<AuthenticatedPeer> {
T::resolve(self, token)
}
}
#[derive(Debug, Default)]
pub struct InMemoryTokenStore {
entries: RwLock<Vec<(CredentialsToken, AuthenticatedPeer)>>,
}
impl InMemoryTokenStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&self, token: CredentialsToken, peer: AuthenticatedPeer) {
let mut entries = self.entries.write().expect("token store lock poisoned");
entries.retain(|(existing, _)| existing != &token);
entries.push((token, peer));
}
pub fn remove(&self, token: &CredentialsToken) {
let mut entries = self.entries.write().expect("token store lock poisoned");
entries.retain(|(existing, _)| existing != token);
}
pub fn rotate(&self, peer_id: &str, new_token: CredentialsToken) -> bool {
let mut entries = self.entries.write().expect("token store lock poisoned");
let Some(index) = entries.iter().position(|(_, p)| p.peer_id == peer_id) else {
return false;
};
let peer = entries[index].1.clone();
entries.remove(index);
entries.push((new_token, peer));
true
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.read().expect("token store lock poisoned").len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl TokenStore for InMemoryTokenStore {
fn resolve(&self, token: &CredentialsToken) -> Option<AuthenticatedPeer> {
let entries = self.entries.read().expect("token store lock poisoned");
entries.iter().find(|(known, _)| known == token).map(|(_, peer)| peer.clone())
}
}
#[derive(Clone, Debug, Default)]
pub struct MountedModules {
modules: Vec<(ModuleId, InterfaceRole)>,
}
impl MountedModules {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, module: ModuleId, role: InterfaceRole) {
if !self.modules.iter().any(|(m, r)| m == &module && *r == role) {
self.modules.push((module, role));
}
}
#[must_use]
pub fn all(&self) -> &[(ModuleId, InterfaceRole)] {
&self.modules
}
#[must_use]
pub fn contains(&self, module: &ModuleId, role: InterfaceRole) -> bool {
self.modules.iter().any(|(m, r)| m.matches(module) && *r == role)
}
}
#[derive(Debug, Default)]
pub struct PeerRegistry {
peers: RwLock<HashMap<String, AuthenticatedPeer>>,
}
impl PeerRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn upsert(&self, peer: AuthenticatedPeer) {
self.peers.write().expect("peer registry lock poisoned").insert(peer.peer_id.clone(), peer);
}
#[must_use]
pub fn get(&self, peer_id: &str) -> Option<AuthenticatedPeer> {
self.peers.read().expect("peer registry lock poisoned").get(peer_id).cloned()
}
pub fn remove(&self, peer_id: &str) -> Option<AuthenticatedPeer> {
self.peers.write().expect("peer registry lock poisoned").remove(peer_id)
}
#[must_use]
pub fn all(&self) -> Vec<AuthenticatedPeer> {
self.peers.read().expect("peer registry lock poisoned").values().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn peer(id: &str, role: TokenRole) -> AuthenticatedPeer {
AuthenticatedPeer {
peer_id: id.to_owned(),
role,
parties: vec![PartyRef::new("NL", "TNM").unwrap()],
version: VersionNumber::V2_3_0,
}
}
#[test]
fn token_a_may_only_reach_credentials_and_versions() {
let bootstrap = peer("p1", TokenRole::A);
assert!(bootstrap.check_scope(&ModuleId::Credentials).is_ok());
assert!(bootstrap.check_scope(&ModuleId::Versions).is_ok());
let err = bootstrap.check_scope(&ModuleId::Locations).unwrap_err();
assert_eq!(err.http_status(), 401);
assert!(peer("p1", TokenRole::C).check_scope(&ModuleId::Locations).is_ok());
}
#[test]
fn writing_to_another_partys_object_is_a_404_not_a_403() {
let p = peer("p1", TokenRole::C);
assert!(p.check_ownership(&PartyRef::new("nl", "tnm").unwrap()).is_ok());
let err = p.check_ownership(&PartyRef::new("DE", "ABC").unwrap()).unwrap_err();
assert_eq!(err.http_status(), 404, "a 404 does not reveal whether the object exists");
}
#[test]
fn the_in_memory_store_resolves_rotates_and_forgets() {
let store = InMemoryTokenStore::new();
let token = CredentialsToken::new("token-c").unwrap();
store.insert(token.clone(), peer("p1", TokenRole::C));
assert_eq!(store.resolve(&token).unwrap().peer_id, "p1");
assert!(store.resolve(&CredentialsToken::new("other").unwrap()).is_none());
let rotated = CredentialsToken::new("token-c2").unwrap();
assert!(store.rotate("p1", rotated.clone()));
assert!(store.resolve(&token).is_none(), "the old token stops working");
assert_eq!(store.resolve(&rotated).unwrap().peer_id, "p1");
assert_eq!(store.len(), 1);
store.remove(&rotated);
assert!(store.is_empty());
}
#[test]
fn mounted_modules_match_the_booking_identifier_either_way() {
let mut mounted = MountedModules::new();
mounted.add(ModuleId::Booking, InterfaceRole::Sender);
mounted.add(ModuleId::Booking, InterfaceRole::Sender);
assert_eq!(mounted.all().len(), 1, "mounting twice is idempotent");
assert!(mounted.contains(&ModuleId::Custom("bookings".into()), InterfaceRole::Sender));
assert!(!mounted.contains(&ModuleId::Booking, InterfaceRole::Receiver));
}
}