1use std::cell::RefCell;
4use std::collections::BTreeMap;
5use std::fmt;
6use std::rc::Rc;
7
8use ravenrs_criteria::{DateTime, Value};
9
10use super::records::{
11 ADMINISTRATORS_ROLE, SECURITY_ROLE, SECURITY_USER, SECURITY_USER_ROLE, is_security_entity,
12 role_from_records,
13};
14use super::{AuditTrail, Identity, Permissions, Role, RoleMerge, hash_password, verify_password};
15use crate::{EntityInfo, Object, ObjectSpace, Query, SpaceError};
16
17pub const MIN_PASSWORD_CHARS: usize = 8;
19const MOST_FAILED_ATTEMPTS: f64 = 5.0;
21const LOCKED_MINUTES: i64 = 15;
23
24#[derive(Clone, Default)]
26#[non_exhaustive]
27#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
28pub struct Credentials {
29 pub name: String,
31 pub password: String,
33 pub extra: BTreeMap<String, String>,
35}
36
37#[cfg_attr(feature = "live", ravenrs_live::expose)]
38impl Credentials {
39 pub fn new(name: impl Into<String>, password: impl Into<String>) -> Self {
41 Self { name: name.into(), password: password.into(), extra: BTreeMap::new() }
42 }
43}
44
45impl fmt::Debug for Credentials {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 f.debug_struct("Credentials").field("name", &self.name).finish_non_exhaustive()
48 }
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53#[non_exhaustive]
54#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
55pub enum AuthError {
56 Rejected,
59 Storage(String),
61}
62
63impl fmt::Display for AuthError {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::Rejected => f.write_str("the name or the password is not right"),
67 Self::Storage(why) => f.write_str(why),
68 }
69 }
70}
71
72impl std::error::Error for AuthError {}
73
74pub trait Authenticator {
77 fn authenticate(&self, credentials: &Credentials) -> Result<Identity, AuthError>;
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
83#[non_exhaustive]
84#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
85pub enum PasswordError {
86 TooShort,
88 WrongCurrent,
90 NotAllowed,
92 NoSuchUser,
94 Failed(String),
96}
97
98impl fmt::Display for PasswordError {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 Self::TooShort => write!(f, "a password has at least {MIN_PASSWORD_CHARS} characters"),
102 Self::WrongCurrent => f.write_str("the current password is not right"),
103 Self::NotAllowed => f.write_str("this password cannot be changed by this person"),
104 Self::NoSuchUser => f.write_str("there is no such user"),
105 Self::Failed(why) => f.write_str(why),
106 }
107 }
108}
109
110impl std::error::Error for PasswordError {}
111
112pub type SpaceMaker = Rc<dyn Fn() -> Box<dyn ObjectSpace>>;
115
116thread_local! {
117 static NOBODY: String = hash_password("nobody has this password").unwrap_or_default();
119}
120
121fn failed(error: SpaceError) -> AuthError {
122 AuthError::Storage(error.to_string())
123}
124
125#[derive(Clone)]
127#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
128pub struct UserDirectory {
129 space: SpaceMaker,
130 roles: Rc<Vec<Role>>,
131}
132
133impl fmt::Debug for UserDirectory {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 f.debug_struct("UserDirectory").field("roles", &self.roles).finish_non_exhaustive()
136 }
137}
138
139#[cfg_attr(feature = "live", ravenrs_live::expose)]
140impl UserDirectory {
141 pub fn new(space: SpaceMaker, roles: Vec<Role>) -> Result<Self, SpaceError> {
144 let directory = Self { space, roles: Rc::new(roles) };
145 directory.ensure_roles()?;
146 Ok(directory)
147 }
148
149 fn ensure_roles(&self) -> Result<(), SpaceError> {
150 let mut space = (self.space)();
151 let existing = space.query(SECURITY_ROLE, &Query::all())?;
152 for role in self.roles.iter() {
153 if !existing.iter().any(|row| row.get("name") == Value::Text(role.name.clone())) {
154 let mut record = space.new_object(SECURITY_ROLE)?;
155 record.set("name", role.name.clone());
156 record.set("administrative", role.administrative);
157 space.save(record)?;
158 }
159 }
160 space.commit()
161 }
162
163 pub fn is_empty(&self) -> Result<bool, SpaceError> {
165 Ok((self.space)().query(SECURITY_USER, &Query::all())?.is_empty())
166 }
167
168 pub fn create_first_administrator(&self, name: &str, password: &str) -> Result<Identity, PasswordError> {
172 let wrong = |error: SpaceError| PasswordError::Failed(error.to_string());
173 if !self.is_empty().map_err(wrong)? {
174 return Err(PasswordError::NotAllowed);
175 }
176 if name.trim().is_empty() {
177 return Err(PasswordError::Failed("a user has a name".to_owned()));
178 }
179 let hash = checked_hash(password)?;
180 let mut space = (self.space)();
181 let roles = space.query(SECURITY_ROLE, &Query::all()).map_err(wrong)?;
182 let role = match roles.iter().find(|row| row.get("administrative") == Value::Bool(true)) {
183 Some(role) => role.get("id"),
184 None => {
185 let mut record = space.new_object(SECURITY_ROLE).map_err(wrong)?;
186 record.set("name", ADMINISTRATORS_ROLE);
187 record.set("administrative", true);
188 let key = record.get("id");
189 space.save(record).map_err(wrong)?;
190 key
191 }
192 };
193 let mut user = space.new_object(SECURITY_USER).map_err(wrong)?;
194 user.set("name", name.trim());
195 user.set("password_hash", hash);
196 user.set("must_change_password", false);
197 let key = user.get("id");
198 space.save(user).map_err(wrong)?;
199 let mut link = space.new_object(SECURITY_USER_ROLE).map_err(wrong)?;
200 link.set("user_id", key.clone());
201 link.set("role_id", role);
202 space.save(link).map_err(wrong)?;
203 space.commit().map_err(wrong)?;
204 self.identity_by_key(&key).map_err(wrong)?.ok_or(PasswordError::NoSuchUser)
205 }
206
207 fn user_named(&self, space: &dyn ObjectSpace, name: &str) -> Result<Option<Object>, SpaceError> {
208 Ok(space
210 .query(SECURITY_USER, &Query::all())?
211 .into_iter()
212 .find(|user| user.get("name") == Value::Text(name.to_owned())))
213 }
214
215 fn roles_of(&self, space: &dyn ObjectSpace, user: &Value) -> Result<Vec<Role>, SpaceError> {
216 let links = space.query(SECURITY_USER_ROLE, &Query::all())?;
217 let records = space.query(SECURITY_ROLE, &Query::all())?;
218 let mut roles = Vec::new();
219 for link in links.iter().filter(|link| link.get("user_id") == *user) {
220 let wanted = link.get("role_id");
221 if let Some(record) = records.iter().find(|record| record.get("id") == wanted) {
222 let role = role_from_records(space, record)?;
223 let declared = self.roles.iter().find(|code| code.name == role.name);
224 roles.push(match declared {
225 Some(code) => role.merged_with(code),
226 None => role,
227 });
228 }
229 }
230 Ok(roles)
231 }
232
233 fn identity_from(&self, space: &dyn ObjectSpace, user: &Object) -> Result<Identity, SpaceError> {
234 let key = user.get("id");
235 let name = match user.get("name") {
236 Value::Text(name) => name,
237 _ => String::new(),
238 };
239 let mut identity = Identity::new(key.clone(), name, self.roles_of(space, &key)?);
240 identity.must_change_password = user.get("must_change_password") == Value::Bool(true);
241 Ok(identity)
242 }
243
244 pub fn identity_of(&self, name: &str) -> Result<Option<Identity>, SpaceError> {
246 let space = (self.space)();
247 match self.user_named(&*space, name)? {
248 Some(user) if user.get("active") == Value::Bool(true) => {
249 self.identity_from(&*space, &user).map(Some)
250 }
251 _ => Ok(None),
252 }
253 }
254
255 pub fn identity_by_key(&self, key: &Value) -> Result<Option<Identity>, SpaceError> {
257 let space = (self.space)();
258 match space.find(SECURITY_USER, key)? {
259 Some(user) if user.get("active") == Value::Bool(true) => {
260 self.identity_from(&*space, &user).map(Some)
261 }
262 _ => Ok(None),
263 }
264 }
265
266 pub(crate) fn has_active_administrator(&self, space: &dyn ObjectSpace) -> Result<bool, SpaceError> {
268 for user in space.query(SECURITY_USER, &Query::all())? {
269 if user.get("active") == Value::Bool(true)
270 && self.roles_of(space, &user.get("id"))?.iter().any(Role::is_administrative)
271 {
272 return Ok(true);
273 }
274 }
275 Ok(false)
276 }
277
278 fn write_password(&self, key: &Value, password: &str, temporary: bool) -> Result<(), PasswordError> {
280 let hash = checked_hash(password)?;
281 let wrong = |error: SpaceError| PasswordError::Failed(error.to_string());
282 let mut space = (self.space)();
283 let mut user = space.find(SECURITY_USER, key).map_err(wrong)?.ok_or(PasswordError::NoSuchUser)?;
284 user.set("password_hash", hash);
285 user.set("must_change_password", temporary);
286 user.set("failed_attempts", 0.0);
287 user.set("locked_until", Value::Null);
288 space.save(user).map_err(wrong)?;
289 space.commit().map_err(wrong)
290 }
291
292 fn check_password(&self, key: &Value, password: &str) -> Result<bool, PasswordError> {
293 let user = (self.space)()
294 .find(SECURITY_USER, key)
295 .map_err(|e| PasswordError::Failed(e.to_string()))?
296 .ok_or(PasswordError::NoSuchUser)?;
297 Ok(matches!(user.get("password_hash"), Value::Text(hash) if verify_password(password, &hash)))
298 }
299}
300
301fn checked_hash(password: &str) -> Result<String, PasswordError> {
302 if password.chars().count() < MIN_PASSWORD_CHARS {
303 return Err(PasswordError::TooShort);
304 }
305 hash_password(password).map_err(PasswordError::Failed)
306}
307
308impl Authenticator for UserDirectory {
309 fn authenticate(&self, credentials: &Credentials) -> Result<Identity, AuthError> {
310 let mut space = (self.space)();
311 let Some(mut user) = self.user_named(&*space, &credentials.name).map_err(failed)? else {
312 NOBODY.with(|hash| verify_password(&credentials.password, hash));
313 return Err(AuthError::Rejected);
314 };
315 let now = crate::local_now();
316 if let Value::Date(until) = user.get("locked_until")
318 && (until.days, until.seconds) > (now.days, now.seconds)
319 {
320 NOBODY.with(|hash| verify_password(&credentials.password, hash));
321 return Err(AuthError::Rejected);
322 }
323 let right = matches!(
324 user.get("password_hash"),
325 Value::Text(hash) if verify_password(&credentials.password, &hash)
326 );
327 if !right {
328 if !matches!(user.get("password_hash"), Value::Text(_)) {
329 NOBODY.with(|hash| verify_password(&credentials.password, hash));
330 }
331 let attempts = match user.get("failed_attempts") {
332 Value::Number(count) => count + 1.0,
333 _ => 1.0,
334 };
335 if attempts >= MOST_FAILED_ATTEMPTS {
336 user.set("failed_attempts", 0.0);
337 let seconds = i64::from(now.seconds) + LOCKED_MINUTES * 60;
338 user.set(
339 "locked_until",
340 Value::Date(DateTime {
341 days: now.days + seconds.div_euclid(86_400),
342 seconds: seconds.rem_euclid(86_400) as u32,
343 }),
344 );
345 } else {
346 user.set("failed_attempts", attempts);
347 }
348 space.save(user).map_err(failed)?;
349 space.commit().map_err(failed)?;
350 return Err(AuthError::Rejected);
351 }
352 if user.get("active") != Value::Bool(true) {
353 return Err(AuthError::Rejected);
354 }
355 if user.get("failed_attempts") != Value::Number(0.0) || user.get("locked_until") != Value::Null {
356 user.set("failed_attempts", 0.0);
357 user.set("locked_until", Value::Null);
358 space.save(user.clone()).map_err(failed)?;
359 space.commit().map_err(failed)?;
360 }
361 self.identity_from(&*space, &user).map_err(failed)
362 }
363}
364
365#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
368pub struct SecurityContext {
369 permissions: RefCell<Permissions>,
370 directory: UserDirectory,
371 entities: Vec<EntityInfo>,
372 merge: RoleMerge,
373 trail: Option<Rc<AuditTrail>>,
374 denied: String,
375}
376
377impl fmt::Debug for SecurityContext {
378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 f.debug_struct("SecurityContext").field("permissions", &self.permissions).finish_non_exhaustive()
380 }
381}
382
383#[cfg_attr(feature = "live", ravenrs_live::expose)]
384impl SecurityContext {
385 pub fn new(
387 identity: Identity,
388 directory: UserDirectory,
389 entities: Vec<EntityInfo>,
390 merge: RoleMerge,
391 ) -> Self {
392 let permissions = Permissions::new(identity, entities.clone(), merge);
393 Self {
394 permissions: RefCell::new(permissions),
395 directory,
396 entities,
397 merge,
398 trail: None,
399 denied: "You are not allowed to do this.".to_owned(),
400 }
401 }
402
403 pub fn with_audit_trail(mut self, trail: Rc<AuditTrail>) -> Self {
405 self.trail = Some(trail);
406 self
407 }
408
409 pub fn with_denied_message(mut self, message: impl Into<String>) -> Self {
411 self.denied = message.into();
412 self
413 }
414
415 pub fn with<T>(&self, ask: impl FnOnce(&Permissions) -> T) -> T {
417 ask(&self.permissions.borrow())
418 }
419
420 pub fn identity(&self) -> Identity {
422 self.permissions.borrow().identity().clone()
423 }
424
425 pub(crate) fn denied(&self) -> SpaceError {
426 SpaceError::Denied(self.denied.clone())
427 }
428
429 pub(crate) fn directory(&self) -> &UserDirectory {
430 &self.directory
431 }
432
433 pub fn reload(&self) -> Result<(), SpaceError> {
436 let current = self.identity();
437 let identity = self
438 .directory
439 .identity_by_key(¤t.key)?
440 .unwrap_or(Identity { roles: Vec::new(), ..current });
441 *self.permissions.borrow_mut() = Permissions::new(identity, self.entities.clone(), self.merge);
442 Ok(())
443 }
444
445 pub fn change_own_password(&self, current: &str, new: &str) -> Result<(), PasswordError> {
447 let key = self.identity().key;
448 let active =
449 self.directory.identity_by_key(&key).map_err(|e| PasswordError::Failed(e.to_string()))?;
450 if active.is_none() {
451 return Err(PasswordError::NotAllowed);
452 }
453 if !self.directory.check_password(&key, current)? {
454 return Err(PasswordError::WrongCurrent);
455 }
456 self.directory.write_password(&key, new, false)?;
457 self.note_password_change(&key);
458 self.reload().map_err(|e| PasswordError::Failed(e.to_string()))
459 }
460
461 pub fn set_password_of(&self, user: &Value, password: &str) -> Result<(), PasswordError> {
463 if !self.with(Permissions::is_administrator) {
464 return Err(PasswordError::NotAllowed);
465 }
466 self.directory.write_password(user, password, true)?;
467 self.note_password_change(user);
468 Ok(())
469 }
470
471 fn note_password_change(&self, user: &Value) {
473 if let Some(trail) = &self.trail {
474 trail.custom(SECURITY_USER, user, "password_hash", "Password changed");
475 }
476 }
477
478 pub(crate) fn touches_security(&self, entity: &str) -> bool {
480 is_security_entity(entity)
481 }
482}