Skip to main content

ravenrs_app_ui/
security.rs

1//! Signing in and managing passwords, on top of the permissions in `ravenrs-app`. What is protected
2//! is protected under this, in the secured space; these screens are the way in.
3
4use std::cell::RefCell;
5use std::rc::Rc;
6
7use ravenrs_app::{
8    Action, ActionCategory, AuditSettings, AuditTrail, AuditedObjectSpace, AuthError, Authenticator,
9    Credentials, EntityInfo, Identity, Message, MessageKind, ModelError, Module, ObjectSpace, PasswordError,
10    Role, RoleMerge, SECURITY_USER, SecuredObjectSpace, SecurityContext, Selection, SpaceMaker,
11    UserDirectory, Value, ViewController, audit_entity, security_entities,
12};
13use ravenrs_layout::{Align, Justify};
14use ravenrs_reactive::{Binding, State};
15use ravenrs_style::{TextStyle, Weight};
16use ravenrs_widgets::{
17    Alert, AnyView, BuildCx, Button, Card, FormField, HStack, Keyed, Localized, NodeId, Text, TextField,
18    Tone, VStack, View, ViewExt,
19};
20
21use crate::model_editor::ModelEditor;
22use crate::session::Words;
23use crate::{App, Language, Session};
24
25/// The id of the action that gives someone a new password.
26pub(crate) const SET_PASSWORD: &str = "SetPassword";
27/// The control of the model editor, on its own view.
28pub(crate) const MODEL_EDITOR: &str = "ModelEditor";
29pub(crate) const MODEL_EDITOR_VIEW: &str = "ModelEditor_DashboardView";
30const CARD_WIDTH: f32 = 380.0;
31
32fn message_of(words: &Words, error: &PasswordError) -> String {
33    match error {
34        PasswordError::TooShort => words.password_too_short.clone(),
35        PasswordError::WrongCurrent => words.password_wrong_current.clone(),
36        other => other.to_string(),
37    }
38}
39
40fn text_binding(state: &State<String>) -> Binding<String> {
41    let (read, write) = (state.clone(), state.clone());
42    Binding::new(move || read.get(), move |value| write.set(value))
43}
44
45type Submit = Rc<dyn Fn(Option<&str>, &str) -> Result<(), PasswordError>>;
46
47/// Asks for a new password, twice, and for the current one when that is what it takes.
48#[derive(Clone)]
49struct PasswordForm {
50    words: Words,
51    current: Option<State<String>>,
52    new: State<String>,
53    repeated: State<String>,
54    error: State<Option<String>>,
55    submit: Submit,
56    done: Rc<dyn Fn()>,
57}
58
59#[cfg_attr(feature = "live", ravenrs_live::expose)]
60impl PasswordForm {
61    fn new(words: Words, asks_current: bool, submit: Submit, done: Rc<dyn Fn()>) -> Self {
62        Self {
63            words,
64            current: asks_current.then(|| State::new(String::new())),
65            new: State::new(String::new()),
66            repeated: State::new(String::new()),
67            error: State::new(None),
68            submit,
69            done,
70        }
71    }
72
73    fn send(&self) {
74        let (new, repeated) = (self.new.peek().clone(), self.repeated.peek().clone());
75        if new != repeated {
76            self.error.set(Some(self.words.passwords_differ.clone()));
77            return;
78        }
79        let current = self.current.as_ref().map(|state| state.peek().clone());
80        match (self.submit)(current.as_deref(), &new) {
81            Ok(()) => {
82                for state in
83                    [Some(&self.new), Some(&self.repeated), self.current.as_ref()].into_iter().flatten()
84                {
85                    state.set(String::new());
86                }
87                self.error.set(None);
88                (self.done)();
89            }
90            Err(error) => self.error.set(Some(message_of(&self.words, &error))),
91        }
92    }
93}
94
95impl View for PasswordForm {
96    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
97        let words = &self.words;
98        let sending = self.clone();
99        let current: Option<AnyView> = self.current.as_ref().map(|state| {
100            Box::new(FormField::new(
101                words.current_password.clone(),
102                TextField::new(text_binding(state)).secure().autofocus(),
103            )) as AnyView
104        });
105        let form = self.clone();
106        let content = VStack::new((
107            self.error.get().map(|error| Alert::new(words.change_password.clone(), error).tone(Tone::Danger)),
108            current,
109            FormField::new(words.new_password.clone(), TextField::new(text_binding(&self.new)).secure()),
110            FormField::new(
111                words.repeat_password.clone(),
112                TextField::new(text_binding(&self.repeated)).secure().on_submit(move |_| form.send()),
113            ),
114            Button::new(words.save_password.clone()).on_click(move || sending.send()),
115        ))
116        .spacing(12.0)
117        .align(Align::Stretch);
118        cx.child(&content)
119    }
120}
121
122/// Users, roles, the audit trail and giving someone a password.
123#[derive(Debug)]
124pub(crate) struct SecurityModule {
125    words: Words,
126    audited: bool,
127}
128
129impl Module for SecurityModule {
130    fn name(&self) -> &str {
131        "Security"
132    }
133
134    fn entities(&self) -> Vec<EntityInfo> {
135        let mut entities = security_entities();
136        if self.audited {
137            entities.push(audit_entity());
138        }
139        entities
140    }
141
142    fn view_controllers(&self) -> Vec<Box<dyn ViewController>> {
143        vec![Box::new(PasswordsController(self.words.clone()))]
144    }
145}
146
147struct PasswordsController(Words);
148
149impl ViewController for PasswordsController {
150    fn id(&self) -> &str {
151        "Passwords"
152    }
153
154    fn actions(&self) -> Vec<Action> {
155        let words = self.0.clone();
156        vec![
157            Action::simple(SET_PASSWORD, words.set_password.clone())
158                .category(ActionCategory::Edit)
159                .target_entity(SECURITY_USER)
160                .selection(Selection::RequireSingle)
161                .icon("key-round")
162                .on_execute(move |cx| {
163                    let session = Session::of(cx).ok_or("this action needs the application's screens")?;
164                    let user = cx.selected.first().map_or(Value::Null, |user| user.get("id"));
165                    show_set_password(&session, user, words.clone());
166                    Ok(())
167                }),
168        ]
169    }
170}
171
172fn show_set_password(session: &Session, user: Value, words: Words) {
173    let (context, shown) = (session.frame().security(), session.clone());
174    let submit: Submit = Rc::new(move |_, password| match &context {
175        Some(context) => context.set_password_of(&user, password),
176        None => Err(PasswordError::NotAllowed),
177    });
178    let told = words.password_saved.clone();
179    let done: Rc<dyn Fn()> = Rc::new(move || {
180        shown.close_presented();
181        shown.show_message(Message::new(told.clone(), MessageKind::Success));
182        shown.records_changed();
183    });
184    let form = PasswordForm::new(words.clone(), false, submit, done);
185    session.present(words.set_password, move || Box::new(form.clone()) as AnyView);
186}
187
188/// Shows the person's own way of changing their password.
189pub(crate) fn show_change_password(session: &Session) {
190    let words = session.words().clone();
191    let (context, shown) = (session.frame().security(), session.clone());
192    let submit: Submit = Rc::new(move |current, password| match &context {
193        Some(context) => context.change_own_password(current.unwrap_or(""), password),
194        None => Err(PasswordError::NotAllowed),
195    });
196    let told = words.password_saved.clone();
197    let done: Rc<dyn Fn()> = Rc::new(move || {
198        shown.close_presented();
199        shown.show_message(Message::new(told.clone(), MessageKind::Success));
200    });
201    let form = PasswordForm::new(words.clone(), true, submit, done);
202    session.present(words.change_password, move || Box::new(form.clone()) as AnyView);
203}
204
205type MakeApp = Rc<dyn Fn(&Identity) -> App>;
206type Seed = Box<dyn FnOnce(&mut dyn ObjectSpace) -> Result<(), String>>;
207
208/// An application people sign in to, each seeing what their roles allow.
209///
210/// Storage has to know [`security_entities`], and [`audit_entity`] with an audit trail. The
211/// application is made anew for each person, once they have signed in, so nothing of its model is
212/// read before that, and every view goes through a [`SecuredObjectSpace`].
213///
214/// ```
215/// use ravenrs_app::{EntityInfo, FieldInfo, FieldKind, MemoryDatabase, security_entities};
216/// use ravenrs_app_ui::{App, SecuredApp};
217///
218/// let account = EntityInfo::new("Account")
219///     .navigation("Records")
220///     .field(FieldInfo::new("id", FieldKind::Integer).key())
221///     .field(FieldInfo::new("name", FieldKind::Text));
222/// let mut entities = security_entities();
223/// entities.push(account.clone());
224/// let database = MemoryDatabase::new(entities)?;
225/// let window = SecuredApp::new("Accounts", move || database.space(), move |_person| {
226///     App::new("Accounts").entity_info(account.clone())
227/// })
228/// .build()?;
229/// # let _ = window;
230/// # Ok::<(), ravenrs_app::ModelError>(())
231/// ```
232#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
233pub struct SecuredApp {
234    space: SpaceMaker,
235    application: MakeApp,
236    language: Language,
237    texts: crate::app::TextPacks,
238    roles: Vec<Role>,
239    merge: RoleMerge,
240    audit: Option<AuditSettings>,
241    authenticator: Option<Rc<dyn Authenticator>>,
242    seed: Option<Seed>,
243    title: String,
244}
245
246impl std::fmt::Debug for SecuredApp {
247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        f.debug_struct("SecuredApp").field("title", &self.title).finish_non_exhaustive()
249    }
250}
251
252#[cfg_attr(feature = "live", ravenrs_live::expose)]
253impl SecuredApp {
254    /// Over what `space` makes, with no permissions of its own. `application` says what the
255    /// application has, for whoever signed in; its storage and seed are replaced by what is here.
256    pub fn new<S: ObjectSpace + 'static>(
257        title: impl Into<String>,
258        space: impl Fn() -> S + 'static,
259        application: impl Fn(&Identity) -> App + 'static,
260    ) -> Self {
261        Self {
262            space: Rc::new(move || Box::new(space()) as Box<dyn ObjectSpace>),
263            application: Rc::new(application),
264            language: Language::default(),
265            texts: Vec::new(),
266            roles: Vec::new(),
267            merge: RoleMerge::default(),
268            audit: None,
269            authenticator: None,
270            seed: None,
271            title: title.into(),
272        }
273    }
274
275    /// The language of its screens.
276    pub fn language(mut self, language: Language) -> Self {
277        self.language = language;
278        self
279    }
280
281    /// Adds or replaces words, as [`App::texts`] does, for the screen of signing in and for the
282    /// application of each person.
283    pub fn texts(mut self, language: impl Into<String>, json: impl Into<String>) -> Self {
284        self.texts.push((language.into(), json.into()));
285        self
286    }
287
288    /// A role declared in code. Its record is created if missing, and it adds to what that says.
289    pub fn role(mut self, role: Role) -> Self {
290        self.roles.push(role);
291        self
292    }
293
294    /// How the roles of one person come together.
295    pub fn merge(mut self, merge: RoleMerge) -> Self {
296        self.merge = merge;
297        self
298    }
299
300    /// Writes who changed what to the audit trail, in the same commit as each change.
301    pub fn audit(mut self, settings: AuditSettings) -> Self {
302        self.audit = Some(settings);
303        self
304    }
305
306    /// Another way of signing in than the passwords it keeps, as with an outside provider.
307    pub fn authenticator(mut self, authenticator: impl Authenticator + 'static) -> Self {
308        self.authenticator = Some(Rc::new(authenticator));
309        self
310    }
311
312    /// What is in its records when it opens, with no permissions in the way, once per run.
313    pub fn seed(mut self, seed: impl FnOnce(&mut dyn ObjectSpace) -> Result<(), String> + 'static) -> Self {
314        self.seed = Some(Box::new(seed));
315        self
316    }
317
318    /// Puts it together, or says what is wrong with how it was declared.
319    pub fn build(self) -> Result<AnyView, ModelError> {
320        let wrong = |error: ravenrs_app::SpaceError| ModelError(error.to_string());
321        let known = (self.space)().entities();
322        for needed in security_entities().into_iter().chain(self.audit.as_ref().map(|_| audit_entity())) {
323            if !known.iter().any(|entity| entity.name == needed.name) {
324                return Err(ModelError(format!("storage has to know the entity {}", needed.name)));
325            }
326        }
327        let directory = UserDirectory::new(self.space.clone(), self.roles.clone()).map_err(wrong)?;
328        if let Some(seed) = self.seed {
329            let mut space = (self.space)();
330            seed(&mut *space).map_err(ModelError)?;
331            space.commit().map_err(wrong)?;
332        }
333        let stage = if directory.is_empty().map_err(wrong)? { Stage::FirstUser } else { Stage::SignIn };
334        let texts = crate::app::texts_of(self.language, &self.texts)?;
335        let words = Words::from_texts(&texts);
336        let authenticator: Rc<dyn Authenticator> = match self.authenticator {
337            Some(authenticator) => authenticator,
338            None => Rc::new(directory.clone()),
339        };
340        let gate = Gate(Rc::new(GateInner {
341            title: self.title,
342            words,
343            language: self.language,
344            texts: self.texts,
345            labels: ravenrs_widgets::Labels::from_texts(&texts),
346            space: self.space,
347            application: self.application,
348            merge: self.merge,
349            audit: self.audit,
350            directory,
351            authenticator,
352            entities: known,
353            stage: State::new(stage),
354            name: State::new(String::new()),
355            password: State::new(String::new()),
356            repeated: State::new(String::new()),
357            error: State::new(None),
358            entered: RefCell::new(0),
359        }));
360        Ok(Box::new(Localized::new(gate.0.labels.clone(), gate)))
361    }
362
363    /// Puts it together, showing on the screen what is wrong with how it was declared.
364    pub fn into_view(self) -> AnyView {
365        self.build().unwrap_or_else(|error| Box::new(Alert::new("RavenRS", error.0).tone(Tone::Danger)))
366    }
367}
368
369#[derive(Clone)]
370enum Stage {
371    /// Nobody can sign in yet.
372    FirstUser,
373    SignIn,
374    /// They signed in with a password they were given.
375    NewPassword(Rc<PasswordForm>),
376    /// The application, for this person.
377    Inside(Rc<AnyView>, u64),
378}
379
380struct GateInner {
381    title: String,
382    words: Words,
383    language: Language,
384    texts: crate::app::TextPacks,
385    labels: ravenrs_widgets::Labels,
386    space: SpaceMaker,
387    application: MakeApp,
388    merge: RoleMerge,
389    audit: Option<AuditSettings>,
390    directory: UserDirectory,
391    authenticator: Rc<dyn Authenticator>,
392    entities: Vec<EntityInfo>,
393    stage: State<Stage>,
394    name: State<String>,
395    password: State<String>,
396    repeated: State<String>,
397    error: State<Option<String>>,
398    entered: RefCell<u64>,
399}
400
401/// What stands between people and the application.
402#[derive(Clone)]
403struct Gate(Rc<GateInner>);
404
405#[cfg_attr(feature = "live", ravenrs_live::expose)]
406impl Gate {
407    fn forget_typed(&self) {
408        self.0.password.set(String::new());
409        self.0.repeated.set(String::new());
410    }
411
412    fn sign_in(&self) {
413        let inner = &self.0;
414        let credentials = Credentials::new(inner.name.peek().clone(), inner.password.peek().clone());
415        let result = inner.authenticator.authenticate(&credentials);
416        self.forget_typed();
417        match result {
418            Ok(identity) => self.enter(identity),
419            Err(AuthError::Rejected) => inner.error.set(Some(inner.words.sign_in_failed.clone())),
420            // What went wrong in storage is not for whoever is at the door.
421            Err(_) => inner.error.set(Some(inner.words.sign_in_unavailable.clone())),
422        }
423    }
424
425    fn create_first(&self) {
426        let inner = &self.0;
427        let (password, repeated) = (inner.password.peek().clone(), inner.repeated.peek().clone());
428        if password != repeated {
429            inner.error.set(Some(inner.words.passwords_differ.clone()));
430            return;
431        }
432        let name = inner.name.peek().clone();
433        match inner.directory.create_first_administrator(&name, &password) {
434            Ok(identity) => {
435                self.forget_typed();
436                self.enter(identity);
437            }
438            Err(error) => inner.error.set(Some(message_of(&inner.words, &error))),
439        }
440    }
441
442    fn enter(&self, identity: Identity) {
443        let inner = &self.0;
444        inner.error.set(None);
445        let must_change = identity.must_change_password;
446        let audit_author = identity.name.clone();
447        let mut context =
448            SecurityContext::new(identity, inner.directory.clone(), inner.entities.clone(), inner.merge)
449                .with_denied_message(inner.words.access_denied.clone());
450        let trail = inner.audit.clone().map(|settings| {
451            let trail = Rc::new(AuditTrail::new(settings, move || audit_author.clone()));
452            trail.write_with(inner.space.clone());
453            trail
454        });
455        if let Some(trail) = &trail {
456            context = context.with_audit_trail(trail.clone());
457        }
458        let context = Rc::new(context);
459        if must_change {
460            let (changing, gate) = (context.clone(), self.clone());
461            let submit: Submit =
462                Rc::new(move |current, new| changing.change_own_password(current.unwrap_or(""), new));
463            let done: Rc<dyn Fn()> = Rc::new(move || gate.open_application(context.clone(), trail.clone()));
464            let form = PasswordForm::new(inner.words.clone(), true, submit, done);
465            inner.stage.set(Stage::NewPassword(Rc::new(form)));
466        } else {
467            self.open_application(context, trail);
468        }
469    }
470
471    fn open_application(&self, context: Rc<SecurityContext>, trail: Option<Rc<AuditTrail>>) {
472        let inner = &self.0;
473        let identity = context.identity();
474        let (raw, secured_context, secured_trail) = (inner.space.clone(), context.clone(), trail.clone());
475        let storage = move || {
476            // Each view reads who they are again, so what someone else changed about them counts.
477            // If that fails they keep what they had, and storage is failing for the view too.
478            let _ = secured_context.reload();
479            let under: Box<dyn ObjectSpace> = match &secured_trail {
480                Some(trail) => Box::new(AuditedObjectSpace::new(raw(), trail.clone())),
481                None => raw(),
482            };
483            SecuredObjectSpace::new(under, secured_context.clone())
484        };
485        let (gate, words) = (self.clone(), inner.words.clone());
486        let audited = trail.is_some();
487        let mut app = (inner.application)(&identity).language(inner.language);
488        for (language, json) in &inner.texts {
489            app = app.texts(language.clone(), json.clone());
490        }
491        let app = app
492            .module(SecurityModule { words: words.clone(), audited })
493            .translation("pt-BR", ravenrs_app_i18n::model::SECURITY_PT_BR)
494            .dashboard(
495                ravenrs_app::DashboardInfo::new(MODEL_EDITOR, words.model_editor.clone())
496                    .navigation("Administration")
497                    .icon("settings")
498                    .control(words.model_editor.clone(), MODEL_EDITOR),
499            )
500            .storage(storage)
501            .secured(context, inner.space.clone())
502            .signed_in(identity.name.clone(), move || gate.sign_out());
503        match app.build() {
504            Ok(view) => {
505                *inner.entered.borrow_mut() += 1;
506                inner.stage.set(Stage::Inside(Rc::new(view), *inner.entered.borrow()));
507            }
508            Err(error) => inner.error.set(Some(error.0)),
509        }
510    }
511
512    fn sign_out(&self) {
513        self.forget_typed();
514        self.0.name.set(String::new());
515        self.0.error.set(None);
516        self.0.stage.set(Stage::SignIn);
517    }
518
519    fn title(&self, detail: Option<&str>) -> AnyView {
520        Box::new(
521            VStack::new((
522                Text::new(self.0.title.clone()).style(TextStyle::Title).font_weight(Weight::SEMIBOLD),
523                detail.map(|text| Text::new(text.to_owned()).style(TextStyle::Caption)),
524            ))
525            .spacing(4.0)
526            .align(Align::Stretch),
527        )
528    }
529
530    fn sign_in_form(&self) -> AnyView {
531        let (words, inner) = (&self.0.words, &self.0);
532        let (by_key, by_button) = (self.clone(), self.clone());
533        Box::new(
534            VStack::new((
535                self.title(None),
536                inner.error.get().map(|error| Alert::message(error).tone(Tone::Danger)),
537                FormField::new(
538                    words.user_name.clone(),
539                    TextField::new(text_binding(&inner.name)).autofocus(),
540                ),
541                FormField::new(
542                    words.password.clone(),
543                    TextField::new(text_binding(&inner.password))
544                        .secure()
545                        .on_submit(move |_| by_key.sign_in()),
546                ),
547                Button::new(words.sign_in.clone()).on_click(move || by_button.sign_in()).full_width(),
548            ))
549            .spacing(12.0)
550            .align(Align::Stretch),
551        )
552    }
553
554    fn first_user_form(&self) -> AnyView {
555        let (words, inner) = (&self.0.words, &self.0);
556        let (by_key, by_button) = (self.clone(), self.clone());
557        Box::new(
558            VStack::new((
559                self.title(Some(&words.first_user)),
560                inner
561                    .error
562                    .get()
563                    .map(|error| Alert::new(words.create_user.clone(), error).tone(Tone::Danger)),
564                FormField::new(
565                    words.user_name.clone(),
566                    TextField::new(text_binding(&inner.name)).autofocus(),
567                ),
568                FormField::new(
569                    words.new_password.clone(),
570                    TextField::new(text_binding(&inner.password)).secure(),
571                ),
572                FormField::new(
573                    words.repeat_password.clone(),
574                    TextField::new(text_binding(&inner.repeated))
575                        .secure()
576                        .on_submit(move |_| by_key.create_first()),
577                ),
578                Button::new(words.create_user.clone()).on_click(move || by_button.create_first()),
579            ))
580            .spacing(12.0)
581            .align(Align::Stretch),
582        )
583    }
584
585    fn new_password_form(&self, form: &PasswordForm) -> AnyView {
586        Box::new(
587            VStack::new((self.title(Some(&self.0.words.must_change_password)), form.clone()))
588                .spacing(12.0)
589                .align(Align::Stretch),
590        )
591    }
592}
593
594impl View for Gate {
595    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
596        let form = match self.0.stage.get() {
597            Stage::Inside(view, entered) => return cx.child(&Keyed::new(entered, SharedView(view))),
598            Stage::FirstUser => self.first_user_form(),
599            Stage::SignIn => self.sign_in_form(),
600            Stage::NewPassword(form) => self.new_password_form(&form),
601        };
602        let centered = HStack::new((Card::new(form).padding(24.0).width(CARD_WIDTH),))
603            .justify(Justify::Center)
604            .align(Align::Center)
605            .grow(1.0);
606        cx.child(&centered)
607    }
608}
609
610/// An application already put together, for the gate to keep.
611struct SharedView(Rc<AnyView>);
612
613impl View for SharedView {
614    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
615        cx.child(&**self.0)
616    }
617}
618
619/// What the model editor's control is made from. It keeps what is being edited between builds.
620pub(crate) fn model_editor(session: &Session) -> impl Fn() -> AnyView + 'static {
621    let editor = Rc::new(ModelEditor::new(session.clone()));
622    move || Box::new(SharedEditor(editor.clone()))
623}
624
625struct SharedEditor(Rc<ModelEditor>);
626
627impl View for SharedEditor {
628    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
629        self.0.build(cx)
630    }
631}