skeg_server_tenant/
lib.rs1#![deny(unsafe_code)]
8
9mod limits;
10
11use std::error::Error;
12use std::path::Path;
13use std::sync::Arc;
14
15use parking_lot::RwLock;
16use skeg_server::{AnonymousPolicy, QuotaAdminError, TenantBackend, TenantId, TenantLimits};
17use skeg_tenant::auth::{Argon2Params, PasswordHash, hash_password_with};
18use skeg_tenant::{AuthStore, TenantId as TenantTenantId};
19
20use crate::limits::LimitsStore;
21
22pub struct AuthStoreBackend {
25 auth: Arc<RwLock<AuthStore>>,
26 limits: RwLock<LimitsStore>,
27 decoy: PasswordHash,
28 strict: bool,
29 admin: Option<TenantTenantId>,
31}
32
33impl AuthStoreBackend {
34 pub fn open(
43 path: impl AsRef<Path>,
44 strict: bool,
45 admin_tenant: Option<&str>,
46 ) -> Result<Arc<Self>, Box<dyn Error>> {
47 let path = path.as_ref();
48 let store = AuthStore::open(path)?;
49 let quotas_path: std::path::PathBuf = format!("{}.quotas", path.to_string_lossy()).into();
50 let limits = LimitsStore::open(quotas_path)?;
51 let decoy = hash_password_with(b"skeg-decoy", Argon2Params::default())?;
54 Ok(Arc::new(Self {
55 auth: Arc::new(RwLock::new(store)),
56 limits: RwLock::new(limits),
57 decoy,
58 strict,
59 admin: admin_tenant.map(TenantTenantId::from_name),
60 }))
61 }
62}
63
64fn tid_to_engine(t: TenantTenantId) -> TenantId {
65 TenantId::from_bytes(*t.as_bytes())
66}
67
68fn tid_from_engine(t: TenantId) -> TenantTenantId {
69 TenantTenantId::from_bytes(*t.as_bytes())
70}
71
72impl TenantBackend for AuthStoreBackend {
73 fn verify_login(&self, user: &str, password: &[u8]) -> Option<TenantId> {
74 self.auth
75 .read()
76 .verify_login(user, password, &self.decoy)
77 .ok()
78 .map(tid_to_engine)
79 }
80
81 fn has_tenant(&self, id: TenantId) -> bool {
82 self.auth.read().has_tenant(tid_from_engine(id))
83 }
84
85 fn anonymous_policy(&self) -> AnonymousPolicy {
86 if self.strict {
87 AnonymousPolicy::Strict
88 } else {
89 AnonymousPolicy::Lenient
90 }
91 }
92
93 fn limits(&self, id: TenantId) -> TenantLimits {
94 let (max_vectors, max_disk_bytes) = self.limits.read().get(*tid_from_engine(id).as_bytes());
95 TenantLimits {
96 max_vectors,
97 max_disk_bytes,
98 }
99 }
100
101 fn is_admin(&self, id: TenantId) -> bool {
102 !id.is_zero() && self.admin == Some(tid_from_engine(id))
103 }
104
105 fn resolve_tenant(&self, name: &str) -> Option<TenantId> {
106 let t = TenantTenantId::from_name(name);
107 self.auth.read().has_tenant(t).then(|| tid_to_engine(t))
108 }
109
110 fn set_limits(&self, id: TenantId, limits: TenantLimits) -> Result<(), QuotaAdminError> {
111 self.limits
112 .write()
113 .set(
114 *tid_from_engine(id).as_bytes(),
115 (limits.max_vectors, limits.max_disk_bytes),
116 )
117 .map_err(|_| QuotaAdminError::Unsupported)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use tempfile::TempDir;
125
126 fn write_auth(dir: &Path, users: &[(&str, &str)]) -> std::path::PathBuf {
128 let path = dir.join("auth.kdb");
129 let mut store = AuthStore::open(&path).unwrap();
130 let hash = hash_password_with(b"pw", Argon2Params::default()).unwrap();
131 for (user, tenant) in users {
132 store.upsert(*user, TenantTenantId::from_name(tenant), hash.clone());
134 }
135 store.save().unwrap();
136 path
137 }
138
139 #[test]
140 fn admin_sets_and_persists_tenant_limits() {
141 let dir = TempDir::new().unwrap();
142 let path = write_auth(dir.path(), &[("admin", "admin"), ("u", "acme")]);
143 let be = AuthStoreBackend::open(&path, false, Some("admin")).unwrap();
144
145 let admin_id = tid_to_engine(TenantTenantId::from_name("admin"));
146 let acme_id = tid_to_engine(TenantTenantId::from_name("acme"));
147
148 assert!(be.is_admin(admin_id));
150 assert!(!be.is_admin(acme_id));
151 assert!(!be.is_admin(TenantId::ZERO));
152
153 assert_eq!(be.resolve_tenant("acme"), Some(acme_id));
155 assert_eq!(be.resolve_tenant("nobody"), None);
156
157 assert_eq!(be.limits(acme_id), TenantLimits::default());
159 be.set_limits(
160 acme_id,
161 TenantLimits {
162 max_vectors: Some(1000),
163 max_disk_bytes: Some(1 << 20),
164 },
165 )
166 .unwrap();
167 assert_eq!(be.limits(acme_id).max_vectors, Some(1000));
168 assert_eq!(be.limits(acme_id).max_disk_bytes, Some(1 << 20));
169
170 drop(be);
172 let be2 = AuthStoreBackend::open(&path, false, Some("admin")).unwrap();
173 assert_eq!(be2.limits(acme_id).max_vectors, Some(1000));
174 }
175
176 #[test]
177 fn no_admin_configured_means_no_admin() {
178 let dir = TempDir::new().unwrap();
179 let path = write_auth(dir.path(), &[("u", "acme")]);
180 let be = AuthStoreBackend::open(&path, false, None).unwrap();
181 let acme_id = tid_to_engine(TenantTenantId::from_name("acme"));
182 assert!(!be.is_admin(acme_id));
183 assert!(be.set_limits(acme_id, TenantLimits::default()).is_ok());
184 }
185}