1use std::sync::Arc;
4
5use clasp_core::security::{Scope, TokenInfo, TokenValidator, ValidationResult};
6
7use crate::entity::Entity;
8use crate::store::EntityStore;
9use crate::token::{parse_token, verify_token_signature, ENTITY_TOKEN_PREFIX};
10
11pub struct EntityValidator {
16 store: Arc<dyn EntityStore>,
17 max_token_age: u64,
19}
20
21impl EntityValidator {
22 pub fn new(store: Arc<dyn EntityStore>) -> Self {
24 Self {
25 store,
26 max_token_age: 0,
27 }
28 }
29
30 pub fn with_max_token_age(mut self, seconds: u64) -> Self {
32 self.max_token_age = seconds;
33 self
34 }
35}
36
37impl TokenValidator for EntityValidator {
38 fn validate(&self, token: &str) -> ValidationResult {
39 if !token.starts_with(ENTITY_TOKEN_PREFIX) {
41 return ValidationResult::NotMyToken;
42 }
43
44 let payload = match parse_token(token) {
46 Ok(p) => p,
47 Err(e) => return ValidationResult::Invalid(format!("malformed entity token: {}", e)),
48 };
49
50 if self.max_token_age > 0 {
52 let now = std::time::SystemTime::now()
53 .duration_since(std::time::UNIX_EPOCH)
54 .map(|d| d.as_secs())
55 .unwrap_or(0);
56
57 if now.saturating_sub(payload.timestamp) > self.max_token_age {
58 return ValidationResult::Expired;
59 }
60 }
61
62 let store = self.store.clone();
66 let entity_id_str = payload.entity_id.clone();
67
68 let entity_id = match crate::entity::EntityId::parse(&entity_id_str) {
69 Ok(id) => id,
70 Err(e) => return ValidationResult::Invalid(format!("invalid entity ID: {}", e)),
71 };
72
73 let entity: Entity = match tokio::task::block_in_place(|| {
75 tokio::runtime::Handle::current().block_on(store.get(&entity_id))
76 }) {
77 Ok(Some(e)) => e,
78 Ok(None) => {
79 return ValidationResult::Invalid(format!("entity not found: {}", entity_id_str))
80 }
81 Err(e) => return ValidationResult::Invalid(format!("store error: {}", e)),
82 };
83
84 if !entity.is_active() {
86 return ValidationResult::Invalid(format!(
87 "entity {} is {}",
88 entity_id_str, entity.status
89 ));
90 }
91
92 if let Err(e) = verify_token_signature(&payload, &entity.public_key) {
94 return ValidationResult::Invalid(format!("signature error: {}", e));
95 }
96
97 let scopes: Vec<Scope> = entity
99 .scopes
100 .iter()
101 .filter_map(|s| Scope::parse(s).ok())
102 .collect();
103
104 let scopes = if scopes.is_empty() && !entity.namespaces.is_empty() {
106 entity
107 .namespaces
108 .iter()
109 .filter_map(|ns: &String| {
110 let pattern = if ns.ends_with("/**") {
111 ns.clone()
112 } else if ns.ends_with('/') {
113 format!("{}**", ns)
114 } else {
115 format!("{}/**", ns)
116 };
117 Scope::parse(&format!("admin:{}", pattern)).ok()
118 })
119 .collect()
120 } else {
121 scopes
122 };
123
124 let info = TokenInfo::new(token.to_string(), scopes)
125 .with_subject(entity_id_str)
126 .with_metadata("entity_type", entity.entity_type.to_string())
127 .with_metadata("entity_name", entity.name);
128
129 ValidationResult::Valid(info)
130 }
131
132 fn name(&self) -> &str {
133 "Entity"
134 }
135
136 fn as_any(&self) -> &dyn std::any::Any {
137 self
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::entity::{EntityKeypair, EntityType};
145 use crate::store::MemoryEntityStore;
146 use crate::token::generate_token;
147
148 #[tokio::test(flavor = "multi_thread")]
149 async fn test_entity_validator_valid() {
150 let store = Arc::new(MemoryEntityStore::new());
151 let keypair = EntityKeypair::generate().unwrap();
152 let mut entity = keypair.to_entity(EntityType::Device, "test-device".to_string());
153 entity.scopes = vec!["admin:/**".to_string()];
154 store.create(&entity).await.unwrap();
155
156 let validator = EntityValidator::new(store);
157 let token = generate_token(&keypair).unwrap();
158
159 match validator.validate(&token) {
160 ValidationResult::Valid(info) => {
161 assert!(info.has_scope(clasp_core::Action::Read, "/any/path"));
162 assert_eq!(info.subject.as_deref(), Some(keypair.entity_id.as_str()));
163 }
164 other => panic!("expected Valid, got {:?}", other),
165 }
166 }
167
168 #[tokio::test(flavor = "multi_thread")]
169 async fn test_entity_validator_not_my_token() {
170 let store = Arc::new(MemoryEntityStore::new());
171 let validator = EntityValidator::new(store);
172
173 match validator.validate("cpsk_some_token") {
174 ValidationResult::NotMyToken => {}
175 other => panic!("expected NotMyToken, got {:?}", other),
176 }
177 }
178
179 #[tokio::test(flavor = "multi_thread")]
180 async fn test_entity_validator_unknown_entity() {
181 let store = Arc::new(MemoryEntityStore::new());
182 let validator = EntityValidator::new(store);
183 let keypair = EntityKeypair::generate().unwrap();
184 let token = generate_token(&keypair).unwrap();
185
186 match validator.validate(&token) {
187 ValidationResult::Invalid(msg) => {
188 assert!(msg.contains("not found"));
189 }
190 other => panic!("expected Invalid, got {:?}", other),
191 }
192 }
193
194 #[tokio::test(flavor = "multi_thread")]
195 async fn test_entity_validator_wrong_signature() {
196 let store = Arc::new(MemoryEntityStore::new());
197 let keypair = EntityKeypair::generate().unwrap();
198 let entity = keypair.to_entity(EntityType::Device, "test-device".to_string());
199 store.create(&entity).await.unwrap();
200
201 let other_keypair = EntityKeypair::generate().unwrap();
203 let token = {
205 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
206 use base64::Engine;
207 use ed25519_dalek::Signer;
208
209 let timestamp = 0u64;
210 let entity_id = keypair.entity_id.as_str().to_string();
211 let mut message = entity_id.as_bytes().to_vec();
212 message.extend_from_slice(×tamp.to_be_bytes());
213
214 let signature = other_keypair.signing_key.sign(&message);
215
216 let payload = crate::token::EntityTokenPayload {
217 entity_id,
218 timestamp,
219 signature: signature.to_bytes().to_vec(),
220 };
221
222 let encoded = rmp_serde::to_vec(&payload).unwrap();
223 format!("ent_{}", URL_SAFE_NO_PAD.encode(&encoded))
224 };
225
226 let validator = EntityValidator::new(store);
227 match validator.validate(&token) {
228 ValidationResult::Invalid(msg) => {
229 assert!(msg.contains("signature"));
230 }
231 other => panic!("expected Invalid, got {:?}", other),
232 }
233 }
234
235 #[tokio::test(flavor = "multi_thread")]
236 async fn test_entity_validator_suspended() {
237 let store = Arc::new(MemoryEntityStore::new());
238 let keypair = EntityKeypair::generate().unwrap();
239 let mut entity = keypair.to_entity(EntityType::Device, "test-device".to_string());
240 entity.status = crate::entity::EntityStatus::Suspended;
241 store.create(&entity).await.unwrap();
242
243 let validator = EntityValidator::new(store);
244 let token = generate_token(&keypair).unwrap();
245
246 match validator.validate(&token) {
247 ValidationResult::Invalid(msg) => {
248 assert!(msg.contains("suspended"));
249 }
250 other => panic!("expected Invalid, got {:?}", other),
251 }
252 }
253
254 #[tokio::test(flavor = "multi_thread")]
255 async fn test_entity_validator_namespace_scopes() {
256 let store = Arc::new(MemoryEntityStore::new());
257 let keypair = EntityKeypair::generate().unwrap();
258 let mut entity = keypair.to_entity(EntityType::Device, "light-controller".to_string());
259 entity.namespaces = vec!["/lights".to_string()];
260 store.create(&entity).await.unwrap();
261
262 let validator = EntityValidator::new(store);
263 let token = generate_token(&keypair).unwrap();
264
265 match validator.validate(&token) {
266 ValidationResult::Valid(info) => {
267 assert!(info.has_scope(clasp_core::Action::Admin, "/lights/room1"));
268 assert!(!info.has_scope(clasp_core::Action::Read, "/audio/mixer"));
269 }
270 other => panic!("expected Valid, got {:?}", other),
271 }
272 }
273
274 #[tokio::test(flavor = "multi_thread")]
277 async fn test_entity_validator_malformed_token() {
278 let store = Arc::new(MemoryEntityStore::new());
279 let validator = EntityValidator::new(store);
280
281 match validator.validate("ent_!!!invalid!!!") {
283 ValidationResult::Invalid(msg) => {
284 assert!(
285 msg.contains("malformed"),
286 "expected malformed error, got: {}",
287 msg
288 );
289 }
290 other => panic!("expected Invalid, got {:?}", other),
291 }
292 }
293
294 #[tokio::test(flavor = "multi_thread")]
295 async fn test_entity_validator_revoked_entity() {
296 let store = Arc::new(MemoryEntityStore::new());
297 let keypair = EntityKeypair::generate().unwrap();
298 let mut entity = keypair.to_entity(EntityType::Device, "test-device".to_string());
299 entity.status = crate::entity::EntityStatus::Revoked;
300 store.create(&entity).await.unwrap();
301
302 let validator = EntityValidator::new(store);
303 let token = generate_token(&keypair).unwrap();
304
305 match validator.validate(&token) {
306 ValidationResult::Invalid(msg) => {
307 assert!(
308 msg.contains("revoked"),
309 "expected revoked error, got: {}",
310 msg
311 );
312 }
313 other => panic!("expected Invalid, got {:?}", other),
314 }
315 }
316
317 #[tokio::test(flavor = "multi_thread")]
318 async fn test_entity_validator_max_token_age() {
319 let store = Arc::new(MemoryEntityStore::new());
320 let keypair = EntityKeypair::generate().unwrap();
321 let entity = keypair.to_entity(EntityType::Device, "test-device".to_string());
322 store.create(&entity).await.unwrap();
323
324 let validator = EntityValidator::new(store).with_max_token_age(1);
326
327 {
330 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
331 use base64::Engine;
332 use ed25519_dalek::Signer;
333
334 let old_timestamp = 1000u64; let entity_id = keypair.entity_id.as_str().to_string();
336 let mut message = entity_id.as_bytes().to_vec();
337 message.extend_from_slice(&old_timestamp.to_be_bytes());
338
339 let signature = keypair.signing_key.sign(&message);
340
341 let payload = crate::token::EntityTokenPayload {
342 entity_id,
343 timestamp: old_timestamp,
344 signature: signature.to_bytes().to_vec(),
345 };
346
347 let encoded = rmp_serde::to_vec(&payload).unwrap();
348 let token = format!("ent_{}", URL_SAFE_NO_PAD.encode(&encoded));
349
350 match validator.validate(&token) {
351 ValidationResult::Expired => {}
352 other => panic!("expected Expired, got {:?}", other),
353 }
354 }
355 }
356
357 #[tokio::test(flavor = "multi_thread")]
358 async fn test_entity_validator_nonexistent_entity() {
359 let store = Arc::new(MemoryEntityStore::new());
360 let validator = EntityValidator::new(store);
361
362 let keypair = EntityKeypair::generate().unwrap();
364 let token = generate_token(&keypair).unwrap();
365
366 match validator.validate(&token) {
367 ValidationResult::Invalid(msg) => {
368 assert!(
369 msg.contains("not found"),
370 "expected not found error, got: {}",
371 msg
372 );
373 }
374 other => panic!("expected Invalid, got {:?}", other),
375 }
376 }
377}