1use std::collections::HashMap;
4use std::sync::Arc;
5use std::sync::RwLock;
6
7use crate::ModuleId;
8use crate::transport::{CredentialsToken, OcpiError, TokenRole};
9use crate::types::PartyRef;
10use crate::{InterfaceRole, VersionNumber};
11
12#[derive(Clone, Debug, PartialEq)]
14pub struct AuthenticatedPeer {
15 pub peer_id: String,
17 pub role: TokenRole,
19 pub parties: Vec<PartyRef>,
21 pub version: VersionNumber,
23}
24
25impl AuthenticatedPeer {
26 #[must_use]
30 pub fn owns(&self, party: &PartyRef) -> bool {
31 self.parties.iter().any(|p| p == party)
32 }
33
34 pub fn check_scope(&self, module: &ModuleId) -> Result<(), OcpiError> {
46 if self.role.may_access(module) { Ok(()) } else { Err(OcpiError::TokenAOutOfScope) }
47 }
48
49 pub fn check_ownership(&self, owner: &PartyRef) -> Result<(), OcpiError> {
64 if self.owns(owner) {
65 return Ok(());
66 }
67 Err(OcpiError::NotFound(format!("{owner} is not a party of the authenticated platform")))
68 }
69}
70
71pub trait TokenStore: Send + Sync + 'static {
78 fn resolve(&self, token: &CredentialsToken) -> Option<AuthenticatedPeer>;
84}
85
86impl<T: TokenStore> TokenStore for Arc<T> {
87 fn resolve(&self, token: &CredentialsToken) -> Option<AuthenticatedPeer> {
88 T::resolve(self, token)
89 }
90}
91
92#[derive(Debug, Default)]
98pub struct InMemoryTokenStore {
99 entries: RwLock<Vec<(CredentialsToken, AuthenticatedPeer)>>,
100}
101
102impl InMemoryTokenStore {
103 #[must_use]
105 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn insert(&self, token: CredentialsToken, peer: AuthenticatedPeer) {
111 let mut entries = self.entries.write().expect("token store lock poisoned");
112 entries.retain(|(existing, _)| existing != &token);
113 entries.push((token, peer));
114 }
115
116 pub fn remove(&self, token: &CredentialsToken) {
118 let mut entries = self.entries.write().expect("token store lock poisoned");
119 entries.retain(|(existing, _)| existing != token);
120 }
121
122 pub fn rotate(&self, peer_id: &str, new_token: CredentialsToken) -> bool {
126 let mut entries = self.entries.write().expect("token store lock poisoned");
127 let Some(index) = entries.iter().position(|(_, p)| p.peer_id == peer_id) else {
128 return false;
129 };
130 let peer = entries[index].1.clone();
131 entries.remove(index);
132 entries.push((new_token, peer));
133 true
134 }
135
136 #[must_use]
138 pub fn len(&self) -> usize {
139 self.entries.read().expect("token store lock poisoned").len()
140 }
141
142 #[must_use]
144 pub fn is_empty(&self) -> bool {
145 self.len() == 0
146 }
147}
148
149impl TokenStore for InMemoryTokenStore {
150 fn resolve(&self, token: &CredentialsToken) -> Option<AuthenticatedPeer> {
151 let entries = self.entries.read().expect("token store lock poisoned");
152 entries.iter().find(|(known, _)| known == token).map(|(_, peer)| peer.clone())
154 }
155}
156
157#[derive(Clone, Debug, Default)]
159pub struct MountedModules {
160 modules: Vec<(ModuleId, InterfaceRole)>,
161}
162
163impl MountedModules {
164 #[must_use]
166 pub fn new() -> Self {
167 Self::default()
168 }
169
170 pub fn add(&mut self, module: ModuleId, role: InterfaceRole) {
172 if !self.modules.iter().any(|(m, r)| m == &module && *r == role) {
173 self.modules.push((module, role));
174 }
175 }
176
177 #[must_use]
179 pub fn all(&self) -> &[(ModuleId, InterfaceRole)] {
180 &self.modules
181 }
182
183 #[must_use]
185 pub fn contains(&self, module: &ModuleId, role: InterfaceRole) -> bool {
186 self.modules.iter().any(|(m, r)| m.matches(module) && *r == role)
187 }
188}
189
190#[derive(Debug, Default)]
195pub struct PeerRegistry {
196 peers: RwLock<HashMap<String, AuthenticatedPeer>>,
197}
198
199impl PeerRegistry {
200 #[must_use]
202 pub fn new() -> Self {
203 Self::default()
204 }
205
206 pub fn upsert(&self, peer: AuthenticatedPeer) {
208 self.peers.write().expect("peer registry lock poisoned").insert(peer.peer_id.clone(), peer);
209 }
210
211 #[must_use]
213 pub fn get(&self, peer_id: &str) -> Option<AuthenticatedPeer> {
214 self.peers.read().expect("peer registry lock poisoned").get(peer_id).cloned()
215 }
216
217 pub fn remove(&self, peer_id: &str) -> Option<AuthenticatedPeer> {
219 self.peers.write().expect("peer registry lock poisoned").remove(peer_id)
220 }
221
222 #[must_use]
224 pub fn all(&self) -> Vec<AuthenticatedPeer> {
225 self.peers.read().expect("peer registry lock poisoned").values().cloned().collect()
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 fn peer(id: &str, role: TokenRole) -> AuthenticatedPeer {
234 AuthenticatedPeer {
235 peer_id: id.to_owned(),
236 role,
237 parties: vec![PartyRef::new("NL", "TNM").unwrap()],
238 version: VersionNumber::V2_3_0,
239 }
240 }
241
242 #[test]
243 fn token_a_may_only_reach_credentials_and_versions() {
244 let bootstrap = peer("p1", TokenRole::A);
245 assert!(bootstrap.check_scope(&ModuleId::Credentials).is_ok());
246 assert!(bootstrap.check_scope(&ModuleId::Versions).is_ok());
247 let err = bootstrap.check_scope(&ModuleId::Locations).unwrap_err();
248 assert_eq!(err.http_status(), 401);
249
250 assert!(peer("p1", TokenRole::C).check_scope(&ModuleId::Locations).is_ok());
251 }
252
253 #[test]
254 fn writing_to_another_partys_object_is_a_404_not_a_403() {
255 let p = peer("p1", TokenRole::C);
256 assert!(p.check_ownership(&PartyRef::new("nl", "tnm").unwrap()).is_ok());
257 let err = p.check_ownership(&PartyRef::new("DE", "ABC").unwrap()).unwrap_err();
258 assert_eq!(err.http_status(), 404, "a 404 does not reveal whether the object exists");
259 }
260
261 #[test]
262 fn the_in_memory_store_resolves_rotates_and_forgets() {
263 let store = InMemoryTokenStore::new();
264 let token = CredentialsToken::new("token-c").unwrap();
265 store.insert(token.clone(), peer("p1", TokenRole::C));
266 assert_eq!(store.resolve(&token).unwrap().peer_id, "p1");
267 assert!(store.resolve(&CredentialsToken::new("other").unwrap()).is_none());
268
269 let rotated = CredentialsToken::new("token-c2").unwrap();
270 assert!(store.rotate("p1", rotated.clone()));
271 assert!(store.resolve(&token).is_none(), "the old token stops working");
272 assert_eq!(store.resolve(&rotated).unwrap().peer_id, "p1");
273 assert_eq!(store.len(), 1);
274
275 store.remove(&rotated);
276 assert!(store.is_empty());
277 }
278
279 #[test]
280 fn mounted_modules_match_the_booking_identifier_either_way() {
281 let mut mounted = MountedModules::new();
282 mounted.add(ModuleId::Booking, InterfaceRole::Sender);
283 mounted.add(ModuleId::Booking, InterfaceRole::Sender);
284 assert_eq!(mounted.all().len(), 1, "mounting twice is idempotent");
285 assert!(mounted.contains(&ModuleId::Custom("bookings".into()), InterfaceRole::Sender));
286 assert!(!mounted.contains(&ModuleId::Booking, InterfaceRole::Receiver));
287 }
288}