Skip to main content

sochdb_query/
capability_token.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// SochDB - LLM-Optimized Embedded Database
3// Copyright (C) 2026 Sushanth Reddy Vanagala (https://github.com/sushanthpy)
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! Capability Tokens + ACLs (Task 8)
19//!
20//! This module implements staged ACLs via capability tokens for the local-first
21//! architecture. The design prioritizes:
22//!
23//! 1. **Simplicity** - Easy to reason about, hard to misapply
24//! 2. **Local-first** - No external auth service required
25//! 3. **Composability** - ACLs integrate with existing filter infrastructure
26//!
27//! ## Token Structure
28//!
29//! ```text
30//! CapabilityToken {
31//!     allowed_namespaces: ["prod", "staging"],
32//!     tenant_id: Option<"acme_corp">,
33//!     project_id: Option<"project_123">,
34//!     capabilities: { read: true, write: false, ... },
35//!     expires_at: 1735689600,
36//!     signature: HMAC-SHA256(...)
37//! }
38//! ```
39//!
40//! ## Verification
41//!
42//! Token verification is O(1):
43//! - HMAC-SHA256 for symmetric tokens
44//! - Ed25519 for asymmetric tokens (cached verification)
45//!
46//! ## Row-Level ACLs (Future)
47//!
48//! Row-level ACL tags become "just another metadata atom":
49//! ```text
50//! HasTag(acl_tag) → bitmap lookup → AllowedSet intersection
51//! ```
52//!
53//! This composes cleanly with existing filter infrastructure.
54
55use std::collections::HashSet;
56use std::time::{Duration, SystemTime, UNIX_EPOCH};
57
58use serde::{Deserialize, Serialize};
59
60use crate::filter_ir::{AuthCapabilities, AuthScope};
61
62// ============================================================================
63// Capability Token
64// ============================================================================
65
66/// A capability token that encodes access permissions
67///
68/// This is the serializable form that can be passed across API boundaries.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct CapabilityToken {
71    /// Token version (for future upgrades)
72    pub version: u8,
73    
74    /// Token ID (for revocation tracking)
75    pub token_id: String,
76    
77    /// Allowed namespaces (non-empty)
78    pub allowed_namespaces: Vec<String>,
79    
80    /// Optional tenant ID
81    pub tenant_id: Option<String>,
82    
83    /// Optional project ID
84    pub project_id: Option<String>,
85    
86    /// Capability flags
87    pub capabilities: TokenCapabilities,
88    
89    /// Issued at (Unix timestamp)
90    pub issued_at: u64,
91    
92    /// Expires at (Unix timestamp)
93    pub expires_at: u64,
94    
95    /// ACL tags the token holder can access (for row-level ACLs)
96    pub acl_tags: Vec<String>,
97    
98    /// Signature (HMAC-SHA256 or Ed25519)
99    pub signature: Vec<u8>,
100}
101
102/// Capability flags in the token
103#[derive(Debug, Clone, Default, Serialize, Deserialize)]
104pub struct TokenCapabilities {
105    /// Can read/query vectors
106    pub can_read: bool,
107    /// Can insert vectors
108    pub can_write: bool,
109    /// Can delete vectors
110    pub can_delete: bool,
111    /// Can perform admin operations (create/drop indexes)
112    pub can_admin: bool,
113    /// Can create new tokens (delegation)
114    pub can_delegate: bool,
115}
116
117impl CapabilityToken {
118    /// Current token version
119    pub const CURRENT_VERSION: u8 = 1;
120    
121    /// Check if the token is expired
122    pub fn is_expired(&self) -> bool {
123        let now = SystemTime::now()
124            .duration_since(UNIX_EPOCH)
125            .map(|d| d.as_secs())
126            .unwrap_or(0);
127        now > self.expires_at
128    }
129    
130    /// Check if a namespace is allowed
131    pub fn is_namespace_allowed(&self, namespace: &str) -> bool {
132        self.allowed_namespaces.iter().any(|ns| ns == namespace)
133    }
134    
135    /// Convert to AuthScope for use with FilterIR
136    pub fn to_auth_scope(&self) -> AuthScope {
137        AuthScope {
138            allowed_namespaces: self.allowed_namespaces.clone(),
139            tenant_id: self.tenant_id.clone(),
140            project_id: self.project_id.clone(),
141            expires_at: Some(self.expires_at),
142            capabilities: AuthCapabilities {
143                can_read: self.capabilities.can_read,
144                can_write: self.capabilities.can_write,
145                can_delete: self.capabilities.can_delete,
146                can_admin: self.capabilities.can_admin,
147            },
148            acl_tags: self.acl_tags.clone(),
149        }
150    }
151    
152    /// Get remaining validity duration
153    pub fn remaining_validity(&self) -> Option<Duration> {
154        let now = SystemTime::now()
155            .duration_since(UNIX_EPOCH)
156            .map(|d| d.as_secs())
157            .unwrap_or(0);
158        
159        if now >= self.expires_at {
160            None
161        } else {
162            Some(Duration::from_secs(self.expires_at - now))
163        }
164    }
165}
166
167// ============================================================================
168// Token Builder
169// ============================================================================
170
171/// Builder for creating capability tokens
172pub struct TokenBuilder {
173    namespaces: Vec<String>,
174    tenant_id: Option<String>,
175    project_id: Option<String>,
176    capabilities: TokenCapabilities,
177    validity: Duration,
178    acl_tags: Vec<String>,
179}
180
181impl TokenBuilder {
182    /// Create a new token builder for a namespace
183    pub fn new(namespace: impl Into<String>) -> Self {
184        Self {
185            namespaces: vec![namespace.into()],
186            tenant_id: None,
187            project_id: None,
188            capabilities: TokenCapabilities {
189                can_read: true,
190                ..Default::default()
191            },
192            validity: Duration::from_secs(3600), // 1 hour default
193            acl_tags: Vec::new(),
194        }
195    }
196    
197    /// Add another namespace
198    pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
199        self.namespaces.push(namespace.into());
200        self
201    }
202    
203    /// Set tenant ID
204    pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
205        self.tenant_id = Some(tenant_id.into());
206        self
207    }
208    
209    /// Set project ID
210    pub fn with_project(mut self, project_id: impl Into<String>) -> Self {
211        self.project_id = Some(project_id.into());
212        self
213    }
214    
215    /// Enable read capability
216    pub fn can_read(mut self) -> Self {
217        self.capabilities.can_read = true;
218        self
219    }
220    
221    /// Enable write capability
222    pub fn can_write(mut self) -> Self {
223        self.capabilities.can_write = true;
224        self
225    }
226    
227    /// Enable delete capability
228    pub fn can_delete(mut self) -> Self {
229        self.capabilities.can_delete = true;
230        self
231    }
232    
233    /// Enable admin capability
234    pub fn can_admin(mut self) -> Self {
235        self.capabilities.can_admin = true;
236        self
237    }
238    
239    /// Enable all capabilities
240    pub fn full_access(mut self) -> Self {
241        self.capabilities = TokenCapabilities {
242            can_read: true,
243            can_write: true,
244            can_delete: true,
245            can_admin: true,
246            can_delegate: false,
247        };
248        self
249    }
250    
251    /// Set validity duration
252    pub fn valid_for(mut self, duration: Duration) -> Self {
253        self.validity = duration;
254        self
255    }
256    
257    /// Add ACL tags
258    pub fn with_acl_tags(mut self, tags: Vec<String>) -> Self {
259        self.acl_tags = tags;
260        self
261    }
262    
263    /// Build the token (unsigned - call sign() on TokenSigner)
264    pub fn build_unsigned(self) -> CapabilityToken {
265        let now = SystemTime::now()
266            .duration_since(UNIX_EPOCH)
267            .map(|d| d.as_secs())
268            .unwrap_or(0);
269        
270        CapabilityToken {
271            version: CapabilityToken::CURRENT_VERSION,
272            token_id: generate_token_id(),
273            allowed_namespaces: self.namespaces,
274            tenant_id: self.tenant_id,
275            project_id: self.project_id,
276            capabilities: self.capabilities,
277            issued_at: now,
278            expires_at: now + self.validity.as_secs(),
279            acl_tags: self.acl_tags,
280            signature: Vec::new(),
281        }
282    }
283}
284
285/// Generate a unique token ID
286fn generate_token_id() -> String {
287    
288    // Simple ID generation - in production use UUID or similar
289    format!("tok_{:x}", 
290        std::time::SystemTime::now()
291            .duration_since(UNIX_EPOCH)
292            .unwrap_or_default()
293            .as_nanos()
294    )
295}
296
297// ============================================================================
298// Token Signing and Verification
299// ============================================================================
300
301/// Token signer using HMAC-SHA256
302pub struct TokenSigner {
303    /// Secret key for HMAC
304    secret: Vec<u8>,
305}
306
307impl TokenSigner {
308    /// Create a new signer with a secret key
309    pub fn new(secret: impl AsRef<[u8]>) -> Self {
310        Self {
311            secret: secret.as_ref().to_vec(),
312        }
313    }
314    
315    /// Sign a token
316    pub fn sign(&self, token: &mut CapabilityToken) {
317        let payload = self.compute_payload(token);
318        token.signature = self.hmac_sha256(&payload);
319    }
320    
321    /// Verify a token signature
322    pub fn verify(&self, token: &CapabilityToken) -> Result<(), TokenError> {
323        // Check version
324        if token.version != CapabilityToken::CURRENT_VERSION {
325            return Err(TokenError::UnsupportedVersion(token.version));
326        }
327        
328        // Check expiry
329        if token.is_expired() {
330            return Err(TokenError::Expired);
331        }
332        
333        // Verify signature
334        let payload = self.compute_payload(token);
335        let expected = self.hmac_sha256(&payload);
336        
337        if !constant_time_eq(&token.signature, &expected) {
338            return Err(TokenError::InvalidSignature);
339        }
340        
341        Ok(())
342    }
343    
344    /// Compute the payload to sign
345    fn compute_payload(&self, token: &CapabilityToken) -> Vec<u8> {
346        // Deterministic serialization of token fields (excluding signature)
347        let mut payload = Vec::new();
348        
349        payload.push(token.version);
350        payload.extend(token.token_id.as_bytes());
351        
352        for ns in &token.allowed_namespaces {
353            payload.extend(ns.as_bytes());
354            payload.push(0); // Separator
355        }
356        
357        if let Some(ref tenant) = token.tenant_id {
358            payload.extend(tenant.as_bytes());
359        }
360        payload.push(0);
361        
362        if let Some(ref project) = token.project_id {
363            payload.extend(project.as_bytes());
364        }
365        payload.push(0);
366        
367        // Capabilities as flags
368        let caps = (token.capabilities.can_read as u8)
369            | ((token.capabilities.can_write as u8) << 1)
370            | ((token.capabilities.can_delete as u8) << 2)
371            | ((token.capabilities.can_admin as u8) << 3)
372            | ((token.capabilities.can_delegate as u8) << 4);
373        payload.push(caps);
374        
375        payload.extend(&token.issued_at.to_le_bytes());
376        payload.extend(&token.expires_at.to_le_bytes());
377        
378        for tag in &token.acl_tags {
379            payload.extend(tag.as_bytes());
380            payload.push(0);
381        }
382        
383        payload
384    }
385    
386    /// HMAC-SHA256
387    fn hmac_sha256(&self, data: &[u8]) -> Vec<u8> {
388        // Simple HMAC implementation
389        // In production, use a proper crypto library
390        use std::collections::hash_map::DefaultHasher;
391        use std::hash::{Hash, Hasher};
392        
393        // This is NOT cryptographically secure - just for demonstration
394        // Use ring, hmac, or sha2 crates in production
395        let mut hasher = DefaultHasher::new();
396        self.secret.hash(&mut hasher);
397        data.hash(&mut hasher);
398        let h1 = hasher.finish();
399        
400        let mut hasher2 = DefaultHasher::new();
401        h1.hash(&mut hasher2);
402        self.secret.hash(&mut hasher2);
403        let h2 = hasher2.finish();
404        
405        let mut result = Vec::with_capacity(16);
406        result.extend(&h1.to_le_bytes());
407        result.extend(&h2.to_le_bytes());
408        result
409    }
410}
411
412/// Constant-time comparison to prevent timing attacks
413fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
414    if a.len() != b.len() {
415        return false;
416    }
417    
418    let mut diff = 0u8;
419    for (x, y) in a.iter().zip(b.iter()) {
420        diff |= x ^ y;
421    }
422    diff == 0
423}
424
425/// Token errors
426#[derive(Debug, Clone, thiserror::Error)]
427pub enum TokenError {
428    #[error("token has expired")]
429    Expired,
430    
431    #[error("invalid signature")]
432    InvalidSignature,
433    
434    #[error("unsupported token version: {0}")]
435    UnsupportedVersion(u8),
436    
437    #[error("token revoked")]
438    Revoked,
439    
440    #[error("namespace not allowed: {0}")]
441    NamespaceNotAllowed(String),
442    
443    #[error("insufficient capabilities")]
444    InsufficientCapabilities,
445}
446
447// ============================================================================
448// Token Revocation (Simple In-Memory)
449// ============================================================================
450
451/// Simple in-memory token revocation list
452pub struct RevocationList {
453    /// Revoked token IDs
454    revoked: std::sync::RwLock<HashSet<String>>,
455}
456
457impl RevocationList {
458    /// Create a new revocation list
459    pub fn new() -> Self {
460        Self {
461            revoked: std::sync::RwLock::new(HashSet::new()),
462        }
463    }
464    
465    /// Revoke a token
466    pub fn revoke(&self, token_id: &str) {
467        self.revoked.write().unwrap().insert(token_id.to_string());
468    }
469    
470    /// Check if a token is revoked
471    pub fn is_revoked(&self, token_id: &str) -> bool {
472        self.revoked.read().unwrap().contains(token_id)
473    }
474    
475    /// Get count of revoked tokens
476    pub fn count(&self) -> usize {
477        self.revoked.read().unwrap().len()
478    }
479}
480
481impl Default for RevocationList {
482    fn default() -> Self {
483        Self::new()
484    }
485}
486
487// ============================================================================
488// Token Validator (Combines Signer + Revocation)
489// ============================================================================
490
491/// Complete token validator
492pub struct TokenValidator {
493    signer: TokenSigner,
494    revocation_list: RevocationList,
495}
496
497impl TokenValidator {
498    /// Create a new validator
499    pub fn new(secret: impl AsRef<[u8]>) -> Self {
500        Self {
501            signer: TokenSigner::new(secret),
502            revocation_list: RevocationList::new(),
503        }
504    }
505    
506    /// Issue a new token
507    pub fn issue(&self, builder: TokenBuilder) -> CapabilityToken {
508        let mut token = builder.build_unsigned();
509        self.signer.sign(&mut token);
510        token
511    }
512    
513    /// Validate a token
514    pub fn validate(&self, token: &CapabilityToken) -> Result<AuthScope, TokenError> {
515        // Check revocation
516        if self.revocation_list.is_revoked(&token.token_id) {
517            return Err(TokenError::Revoked);
518        }
519        
520        // Verify signature and expiry
521        self.signer.verify(token)?;
522        
523        // Convert to AuthScope
524        Ok(token.to_auth_scope())
525    }
526    
527    /// Revoke a token
528    pub fn revoke(&self, token_id: &str) {
529        self.revocation_list.revoke(token_id);
530    }
531}
532
533// ============================================================================
534// Row-Level ACL Tags (Future Extension)
535// ============================================================================
536
537/// A row-level ACL tag
538/// 
539/// In the future, documents can have ACL tags and tokens can specify
540/// which tags they can access. This integrates with the filter IR:
541///
542/// ```text
543/// FilterAtom::HasTag("confidential") → bitmap lookup → intersection
544/// ```
545#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
546pub struct AclTag(String);
547
548impl AclTag {
549    /// Create a new ACL tag
550    pub fn new(tag: impl Into<String>) -> Self {
551        Self(tag.into())
552    }
553    
554    /// Get the tag name
555    pub fn name(&self) -> &str {
556        &self.0
557    }
558}
559
560/// ACL tag index for row-level security
561/// 
562/// This would be integrated with MetadataIndex to provide:
563/// tag → bitmap of doc_ids with that tag
564#[derive(Debug, Default)]
565pub struct AclTagIndex {
566    /// Map from tag to doc_ids
567    tag_to_docs: std::collections::HashMap<String, Vec<u64>>,
568}
569
570impl AclTagIndex {
571    /// Create a new ACL tag index
572    pub fn new() -> Self {
573        Self::default()
574    }
575    
576    /// Add a tag to a document
577    pub fn add_tag(&mut self, doc_id: u64, tag: &str) {
578        self.tag_to_docs
579            .entry(tag.to_string())
580            .or_default()
581            .push(doc_id);
582    }
583    
584    /// Get doc_ids with a specific tag
585    pub fn docs_with_tag(&self, tag: &str) -> &[u64] {
586        self.tag_to_docs.get(tag).map(|v| v.as_slice()).unwrap_or(&[])
587    }
588    
589    /// Get doc_ids accessible by a set of allowed tags (union)
590    pub fn accessible_docs(&self, allowed_tags: &[String]) -> Vec<u64> {
591        let mut result = HashSet::new();
592        for tag in allowed_tags {
593            if let Some(docs) = self.tag_to_docs.get(tag) {
594                result.extend(docs.iter().copied());
595            }
596        }
597        result.into_iter().collect()
598    }
599}
600
601// ============================================================================
602// Tests
603// ============================================================================
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608    
609    #[test]
610    fn test_token_builder() {
611        let token = TokenBuilder::new("production")
612            .with_namespace("staging")
613            .with_tenant("acme")
614            .can_read()
615            .can_write()
616            .valid_for(Duration::from_secs(3600))
617            .build_unsigned();
618        
619        assert_eq!(token.allowed_namespaces.len(), 2);
620        assert_eq!(token.tenant_id, Some("acme".to_string()));
621        assert!(token.capabilities.can_read);
622        assert!(token.capabilities.can_write);
623        assert!(!token.capabilities.can_delete);
624    }
625    
626    #[test]
627    fn test_token_signing_and_verification() {
628        let signer = TokenSigner::new("super_secret_key");
629        
630        let mut token = TokenBuilder::new("production")
631            .can_read()
632            .valid_for(Duration::from_secs(3600))
633            .build_unsigned();
634        
635        signer.sign(&mut token);
636        assert!(!token.signature.is_empty());
637        
638        // Verification should succeed
639        assert!(signer.verify(&token).is_ok());
640        
641        // Tamper with token
642        token.allowed_namespaces.push("hacked".to_string());
643        assert!(signer.verify(&token).is_err());
644    }
645    
646    #[test]
647    fn test_token_expiry() {
648        // Create a token that expires 1 second in the past
649        let mut token = TokenBuilder::new("production")
650            .valid_for(Duration::from_secs(3600))
651            .build_unsigned();
652        
653        // Manually set expires_at to 0 (Unix epoch - in the past)
654        token.expires_at = 0;
655        
656        assert!(token.is_expired());
657    }
658    
659    #[test]
660    fn test_token_to_auth_scope() {
661        let token = TokenBuilder::new("production")
662            .with_tenant("acme")
663            .can_read()
664            .can_write()
665            .with_acl_tags(vec!["public".to_string(), "internal".to_string()])
666            .build_unsigned();
667        
668        let scope = token.to_auth_scope();
669        assert!(scope.is_namespace_allowed("production"));
670        assert!(!scope.is_namespace_allowed("staging"));
671        assert_eq!(scope.tenant_id, Some("acme".to_string()));
672        assert!(scope.capabilities.can_read);
673        assert!(scope.capabilities.can_write);
674        assert_eq!(scope.acl_tags.len(), 2);
675    }
676    
677    #[test]
678    fn test_revocation() {
679        let validator = TokenValidator::new("secret");
680        
681        let token = validator.issue(
682            TokenBuilder::new("production")
683                .can_read()
684                .valid_for(Duration::from_secs(3600))
685        );
686        
687        // Should validate
688        assert!(validator.validate(&token).is_ok());
689        
690        // Revoke
691        validator.revoke(&token.token_id);
692        
693        // Should fail validation
694        assert!(matches!(
695            validator.validate(&token),
696            Err(TokenError::Revoked)
697        ));
698    }
699    
700    #[test]
701    fn test_acl_tag_index() {
702        let mut index = AclTagIndex::new();
703        
704        index.add_tag(1, "public");
705        index.add_tag(2, "public");
706        index.add_tag(3, "internal");
707        index.add_tag(4, "confidential");
708        
709        assert_eq!(index.docs_with_tag("public").len(), 2);
710        assert_eq!(index.docs_with_tag("internal").len(), 1);
711        
712        let accessible = index.accessible_docs(&["public".to_string(), "internal".to_string()]);
713        assert_eq!(accessible.len(), 3);
714    }
715}