Skip to main content

ravenrs_app/security/
directory.rs

1//! Signing in and the people who can: users, passwords and being locked out.
2
3use 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
17/// The shortest password taken, in characters.
18pub const MIN_PASSWORD_CHARS: usize = 8;
19/// Wrong passwords in a row before an account waits.
20const MOST_FAILED_ATTEMPTS: f64 = 5.0;
21/// How long it waits, in minutes.
22const LOCKED_MINUTES: i64 = 15;
23
24/// What someone gave to sign in.
25#[derive(Clone, Default)]
26#[non_exhaustive]
27#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
28pub struct Credentials {
29    /// Their name.
30    pub name: String,
31    /// Their password.
32    pub password: String,
33    /// Anything else another way of signing in asks for.
34    pub extra: BTreeMap<String, String>,
35}
36
37#[cfg_attr(feature = "live", ravenrs_live::expose)]
38impl Credentials {
39    /// A name and a password.
40    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/// Why someone could not sign in.
52#[derive(Clone, Debug, PartialEq, Eq)]
53#[non_exhaustive]
54#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
55pub enum AuthError {
56    /// The name, the password or the state of the account did not fit. It never says which, so
57    /// this cannot be used to find out who has an account.
58    Rejected,
59    /// Storage failed.
60    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
74/// A way of signing in. Passwords are [`UserDirectory`]; a way that talks to an outside provider,
75/// as OAuth 2 does, implements this and asks [`UserDirectory::identity_of`] for the roles.
76pub trait Authenticator {
77    /// Who this is, or why not.
78    fn authenticate(&self, credentials: &Credentials) -> Result<Identity, AuthError>;
79}
80
81/// Why a password could not be set.
82#[derive(Clone, Debug, PartialEq, Eq)]
83#[non_exhaustive]
84#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
85pub enum PasswordError {
86    /// It is shorter than [`MIN_PASSWORD_CHARS`].
87    TooShort,
88    /// The current password given is not right.
89    WrongCurrent,
90    /// This person cannot change that password.
91    NotAllowed,
92    /// There is no such user.
93    NoSuchUser,
94    /// Something else went wrong.
95    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
112/// Makes a unit of work with no permissions around it. It only ever goes to what needs to read
113/// before anyone has signed in, or to what the security system does itself.
114pub type SpaceMaker = Rc<dyn Fn() -> Box<dyn ObjectSpace>>;
115
116thread_local! {
117    /// What a password is compared with when there is no such user, so that takes as long.
118    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/// The people who can sign in, kept as records.
126#[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    /// Over what this makes. `roles` are what the application declares in code: a role record of
142    /// the same name gets what they say, and one is created if missing.
143    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    /// Whether nobody can sign in yet, so the first person has to be created.
164    pub fn is_empty(&self) -> Result<bool, SpaceError> {
165        Ok((self.space)().query(SECURITY_USER, &Query::all())?.is_empty())
166    }
167
168    /// Creates the first person, as an administrator. It only works while there is nobody, so it
169    /// cannot be used to get in later. Whoever opens a new installation first gets this, so it
170    /// is for a program on someone's machine: a server creates its first user in its setup.
171    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        // Compared here, so a name never becomes part of an expression.
209        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    /// Who this name is, if they can sign in, for a way of signing in that checked them itself.
245    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    /// Who this key is, as they stand now; nobody once they cannot sign in.
256    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    /// Whether someone active is left who can do everything, in what this space holds.
267    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    /// Sets the password of a user, with no questions: who may do this is up to the caller.
279    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        // While it waits the password is not even looked at, so guessing goes nowhere.
317        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/// Who is signed in and what the security system needs to answer for them. One per signed-in
366/// person, shared by their units of work.
367#[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    /// For this person, over these entities.
386    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    /// Writes what the security system does itself there.
404    pub fn with_audit_trail(mut self, trail: Rc<AuditTrail>) -> Self {
405        self.trail = Some(trail);
406        self
407    }
408
409    /// What people are told when something is refused.
410    pub fn with_denied_message(mut self, message: impl Into<String>) -> Self {
411        self.denied = message.into();
412        self
413    }
414
415    /// Asks what this person can do.
416    pub fn with<T>(&self, ask: impl FnOnce(&Permissions) -> T) -> T {
417        ask(&self.permissions.borrow())
418    }
419
420    /// Who is signed in.
421    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    /// Reads who they are again, after their records may have changed: losing a role, or being
434    /// turned off, takes effect now. Someone no longer active keeps no permission at all.
435    pub fn reload(&self) -> Result<(), SpaceError> {
436        let current = self.identity();
437        let identity = self
438            .directory
439            .identity_by_key(&current.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    /// Changes this person's own password, which takes the current one.
446    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    /// An administrator gives someone a password, which they have to change when they sign in.
462    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    /// The hash itself never goes in the trail, only that it happened.
472    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    /// Whether a change touches what decides who can do what.
479    pub(crate) fn touches_security(&self, entity: &str) -> bool {
480        is_security_entity(entity)
481    }
482}