[fractal/fractal-next] auth-data: Add dialog to ask for authentication



commit e25cb64d90fc829688a98cd18df9789a1ed4434c
Author: Julian Sparber <julian sparber net>
Date:   Tue Sep 21 17:47:18 2021 +0200

    auth-data: Add dialog to ask for authentication
    
    This is the base for
    https://gitlab.gnome.org/GNOME/fractal/-/issues/835, but does only
    implement Authentication via Password and the Browser Fallback.

 data/resources/resources.gresource.xml      |   1 +
 data/resources/ui/components-auth-dialog.ui | 132 ++++++++++
 po/POTFILES.in                              |   2 +
 src/components/auth_dialog.rs               | 383 ++++++++++++++++++++++++++++
 src/components/mod.rs                       |   2 +
 src/meson.build                             |   1 +
 6 files changed, 521 insertions(+)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 088f12e9..874d4dbd 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -34,6 +34,7 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="in-app-notification.ui">ui/in-app-notification.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="components-avatar.ui">ui/components-avatar.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="avatar-with-selection.ui">ui/avatar-with-selection.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="components-auth-dialog.ui">ui/components-auth-dialog.ui</file>
     <file compressed="true">style.css</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/welcome.svg</file>
diff --git a/data/resources/ui/components-auth-dialog.ui b/data/resources/ui/components-auth-dialog.ui
new file mode 100644
index 00000000..66cc4226
--- /dev/null
+++ b/data/resources/ui/components-auth-dialog.ui
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ComponentsAuthDialog" parent="AdwWindow">
+    <property name="modal">true</property>
+    <property name="hide-on-close">true</property>
+    <property name="title"/>
+    <property name="resizable">0</property>
+    <property name="default-widget">button_ok</property>
+    <style>
+      <class name="message"/>
+      <class name="dialog"/>
+    </style>
+    <child>
+      <object class="GtkBox">
+        <property name="spacing">12</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="halign">center</property>
+            <property name="label" translatable="yes">Authentication</property>
+            <property name="margin-top">24</property>
+            <style>
+              <class name="title-2"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="hhomogeneous">False</property>
+            <property name="vhomogeneous">False</property>
+            <property name="margin-bottom">12</property>
+            <property name="margin-start">24</property>
+            <property name="margin-end">24</property>
+            <child>
+              <object class="GtkStackPage">
+                <property name="name">m.login.password</property>
+                <property name="child">
+                  <object class="GtkBox">
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">12</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Please authenticate the operation with 
your password</property>
+                        <property name="wrap">True</property>
+                        <property name="wrap-mode">word-char</property>
+                        <property name="max-width-chars">60</property>
+                        <property name="halign">center</property>
+                        <property name="valign">start</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkPasswordEntry" id="password">
+                        <property name="activates-default">True</property>
+                        <property name="show-peek-icon">True</property>
+                      </object>
+                    </child>
+                  </object>
+                </property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkStackPage">
+                <property name="name">fallback</property>
+                <property name="child">
+                  <object class="GtkBox">
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">12</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Please authenticate the operation via the 
browser and once completed press confirm.</property>
+                        <property name="wrap">True</property>
+                        <property name="wrap-mode">word-char</property>
+                        <property name="max-width-chars">60</property>
+                        <property name="halign">center</property>
+                        <property name="valign">start</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="open_browser_btn">
+                        <property name="label" translatable="yes">Authenticate via Browser</property>
+                        <property name="halign">center</property>
+                        <style>
+                          <class name="suggested-action"/>
+                          <class name="pill-button"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                </property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="error">
+            <property name="visible">False</property>
+            <property name="wrap">True</property>
+            <property name="wrap-mode">word-char</property>
+            <property name="max-width-chars">60</property>
+            <property name="halign">center</property>
+            <property name="valign">start</property>
+            <property name="margin-bottom">12</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="hexpand">True</property>
+            <property name="homogeneous">True</property>
+            <property name="halign">fill</property>
+            <child>
+              <object class="GtkButton" id="button_cancel">
+                <property name="label" translatable="yes">Cancel</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="button_ok">
+                <property name="label" translatable="yes">Confirm</property>
+                <style>
+                  <class name="suggested-action"/>
+                </style>
+              </object>
+            </child>
+            <style>
+              <class name="dialog-action-area"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
+
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 2086295f..4dc61b5d 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -6,6 +6,7 @@ data/org.gnome.FractalNext.metainfo.xml.in.in
 
 # UI files
 data/resources/ui/add_account.ui
+data/resources/ui/components-auth-dialog.ui
 data/resources/ui/components-avatar.ui
 data/resources/ui/avatar-with-selection.ui
 data/resources/ui/content-divider-row.ui
@@ -36,6 +37,7 @@ data/resources/ui/window.ui
 
 # Rust files
 src/application.rs
+src/components/auth_dialog.rs
 src/components/avatar.rs
 src/components/context_menu_bin.rs
 src/components/custom_entry.rs
diff --git a/src/components/auth_dialog.rs b/src/components/auth_dialog.rs
new file mode 100644
index 00000000..42067862
--- /dev/null
+++ b/src/components/auth_dialog.rs
@@ -0,0 +1,383 @@
+use adw::subclass::prelude::*;
+use gtk::gdk;
+use gtk::gio::prelude::*;
+use gtk::glib::clone;
+use gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::{glib, CompositeTemplate};
+use std::cell::Cell;
+use std::future::Future;
+
+use crate::session::Session;
+use crate::session::UserExt;
+use crate::RUNTIME;
+
+use matrix_sdk::{
+    ruma::api::{
+        client::{
+            error::ErrorBody,
+            r0::uiaa::{
+                AuthData as MatrixAuthData,
+                FallbackAcknowledgement as MatrixFallbackAcknowledgement,
+                Password as MatrixPassword, UiaaInfo, UiaaResponse, UserIdentifier,
+            },
+        },
+        error::{FromHttpResponseError, ServerError},
+        OutgoingRequest,
+    },
+    ruma::assign,
+    HttpError,
+    HttpError::UiaaError,
+    HttpResult,
+};
+
+use std::fmt::Debug;
+
+pub struct Password {
+    pub user_id: String,
+    pub password: String,
+    pub session: Option<String>,
+}
+
+pub struct FallbackAcknowledgement {
+    pub session: String,
+}
+
+// FIXME: we can't move the ruma AuthData between threads
+// because it's not owned data and doesn't live long enough.
+// Therefore we have our own AuthData.
+pub enum AuthData {
+    Password(Password),
+    FallbackAcknowledgement(FallbackAcknowledgement),
+}
+
+impl AuthData {
+    pub fn as_matrix_auth_data(&self) -> MatrixAuthData {
+        match self {
+            AuthData::Password(Password {
+                user_id,
+                password,
+                session,
+            }) => MatrixAuthData::Password(assign!(MatrixPassword::new(
+                                UserIdentifier::MatrixId(&user_id),
+                                &password,
+                            ), { session: session.as_deref() })),
+            AuthData::FallbackAcknowledgement(FallbackAcknowledgement { session }) => {
+                MatrixAuthData::FallbackAcknowledgement(MatrixFallbackAcknowledgement::new(
+                    &session,
+                ))
+            }
+        }
+    }
+}
+
+mod imp {
+    use super::*;
+    use glib::subclass::{InitializingObject, Signal};
+    use glib::SignalHandlerId;
+    use once_cell::sync::Lazy;
+    use std::cell::RefCell;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/components-auth-dialog.ui")]
+    pub struct AuthDialog {
+        pub session: RefCell<Option<Session>>,
+        #[template_child]
+        pub stack: TemplateChild<gtk::Stack>,
+        #[template_child]
+        pub password: TemplateChild<gtk::PasswordEntry>,
+        #[template_child]
+        pub error: TemplateChild<gtk::Label>,
+
+        #[template_child]
+        pub button_cancel: TemplateChild<gtk::Button>,
+        #[template_child]
+        pub button_ok: TemplateChild<gtk::Button>,
+
+        #[template_child]
+        pub open_browser_btn: TemplateChild<gtk::Button>,
+        pub open_browser_btn_handler: RefCell<Option<SignalHandlerId>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for AuthDialog {
+        const NAME: &'static str = "ComponentsAuthDialog";
+        type Type = super::AuthDialog;
+        type ParentType = adw::Window;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+            let response = glib::Variant::from_tuple(&[false.to_variant()]);
+            klass.add_binding_signal(
+                gdk::keys::constants::Escape,
+                gdk::ModifierType::empty(),
+                "response",
+                Some(&response),
+            );
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for AuthDialog {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![glib::ParamSpec::new_object(
+                    "session",
+                    "Session",
+                    "The session",
+                    Session::static_type(),
+                    glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                )]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "session" => obj.set_session(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "session" => obj.session().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+
+            self.button_cancel
+                .connect_clicked(clone!(@weak obj => move |_| {
+                    obj.emit_by_name("response", &[&false]).unwrap();
+                }));
+
+            self.button_ok
+                .connect_clicked(clone!(@weak obj => move |_| {
+                    obj.emit_by_name("response", &[&true]).unwrap();
+                }));
+
+            obj.connect_close_request(
+                clone!(@weak obj => @default-return gtk::Inhibit(false), move |_| {
+                    obj.emit_by_name("response", &[&false]).unwrap();
+                    gtk::Inhibit(false)
+                }),
+            );
+        }
+
+        fn signals() -> &'static [Signal] {
+            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
+                vec![Signal::builder(
+                    "response",
+                    &[bool::static_type().into()],
+                    <()>::static_type().into(),
+                )
+                .action()
+                .build()]
+            });
+            SIGNALS.as_ref()
+        }
+    }
+    impl WidgetImpl for AuthDialog {}
+    impl WindowImpl for AuthDialog {}
+    impl AdwWindowImpl for AuthDialog {}
+}
+
+glib::wrapper! {
+    /// Button showing a spinner, revealing its label once loaded.
+    pub struct AuthDialog(ObjectSubclass<imp::AuthDialog>)
+        @extends gtk::Widget, adw::Window, gtk::Dialog, gtk::Window, @implements gtk::Accessible;
+}
+
+impl AuthDialog {
+    pub fn new(transient_for: Option<&impl IsA<gtk::Window>>, session: &Session) -> Self {
+        glib::Object::new(&[("transient-for", &transient_for), ("session", session)])
+            .expect("Failed to create AuthDialog")
+    }
+
+    pub fn session(&self) -> Option<Session> {
+        let priv_ = imp::AuthDialog::from_instance(self);
+        priv_.session.borrow().clone()
+    }
+
+    pub fn set_session(&self, session: Option<Session>) {
+        let priv_ = imp::AuthDialog::from_instance(self);
+
+        if self.session() == session {
+            return;
+        };
+
+        priv_.session.replace(session);
+
+        self.notify("session");
+    }
+
+    pub async fn authenticate<
+        Request: Send + 'static,
+        F1: Future<Output = HttpResult<Request::IncomingResponse>> + Send + 'static,
+        FN: Fn(Option<AuthData>) -> F1 + Send + Sync + 'static + Clone,
+    >(
+        &self,
+        callback: FN,
+    ) -> Option<HttpResult<Request::IncomingResponse>>
+    where
+        Request: OutgoingRequest + Debug,
+        Request::IncomingResponse: Send,
+        HttpError: From<FromHttpResponseError<Request::EndpointError>>,
+    {
+        let priv_ = imp::AuthDialog::from_instance(self);
+        let mut auth_data = None;
+
+        loop {
+            let callback_clone = callback.clone();
+            let (sender, receiver) = futures::channel::oneshot::channel();
+            RUNTIME.spawn(async move { sender.send(callback_clone(auth_data).await) });
+            let response = receiver.await.unwrap();
+
+            let uiaa_info: UiaaInfo = match response {
+                Ok(result) => return Some(Ok(result)),
+                Err(UiaaError(FromHttpResponseError::Http(ServerError::Known(
+                    UiaaResponse::AuthResponse(uiaa_info),
+                )))) => uiaa_info,
+                Err(error) => return Some(Err(error)),
+            };
+
+            self.show_auth_error(&uiaa_info.auth_error);
+
+            // Find the first flow that matches the completed flow
+            let flow = uiaa_info
+                .flows
+                .iter()
+                .find(|flow| flow.stages.starts_with(&uiaa_info.completed))?;
+
+            match flow.stages[uiaa_info.completed.len()].as_str() {
+                "m.login.password" => {
+                    priv_.stack.set_visible_child_name("m.login.password");
+                    if self.show_and_wait_for_response().await {
+                        let user_id = self
+                            .session()
+                            .unwrap()
+                            .user()
+                            .unwrap()
+                            .user_id()
+                            .to_string();
+                        let password = priv_.password.text().to_string();
+                        let session = uiaa_info.session;
+
+                        auth_data = Some(AuthData::Password(Password {
+                            user_id,
+                            password,
+                            session,
+                        }));
+
+                        continue;
+                    }
+                }
+                // TODO implement other authentication types
+                // See: https://gitlab.gnome.org/GNOME/fractal/-/issues/835
+                _ => {
+                    if let Some(session) = uiaa_info.session {
+                        priv_.stack.set_visible_child_name("fallback");
+
+                        let client = self.session()?.client().clone();
+                        let (sender, receiver) = futures::channel::oneshot::channel();
+                        RUNTIME.spawn(async move { sender.send(client.homeserver().await) });
+                        let homeserver = receiver.await.unwrap();
+                        self.setup_fallback_page(
+                            homeserver.as_str(),
+                            &flow.stages.first()?,
+                            &session,
+                        );
+                        if self.show_and_wait_for_response().await {
+                            auth_data =
+                                Some(AuthData::FallbackAcknowledgement(FallbackAcknowledgement {
+                                    session,
+                                }));
+
+                            continue;
+                        }
+                    }
+                }
+            }
+
+            return None;
+        }
+    }
+
+    async fn show_and_wait_for_response(&self) -> bool {
+        let (sender, receiver) = futures::channel::oneshot::channel();
+        let sender = Cell::new(Some(sender));
+
+        let handler_id = self.connect_response(move |_, response| {
+            if let Some(sender) = sender.take() {
+                sender.send(response).unwrap();
+            }
+        });
+
+        self.show();
+
+        let result = receiver.await.unwrap();
+        self.disconnect(handler_id);
+        self.close();
+
+        result
+    }
+
+    fn show_auth_error(&self, auth_error: &Option<ErrorBody>) {
+        let priv_ = imp::AuthDialog::from_instance(self);
+
+        if let Some(auth_error) = auth_error {
+            priv_.error.set_label(&auth_error.message);
+            priv_.error.show();
+        } else {
+            priv_.error.hide();
+        }
+    }
+
+    fn setup_fallback_page(&self, homeserver: &str, auth_type: &str, session: &str) {
+        let priv_ = imp::AuthDialog::from_instance(self);
+
+        if let Some(handler) = priv_.open_browser_btn_handler.take() {
+            priv_.open_browser_btn.disconnect(handler);
+        }
+
+        let uri = format!(
+            "{}_matrix/client/r0/auth/{}/fallback/web?session={}",
+            homeserver, auth_type, session
+        );
+
+        let handler =
+            priv_
+                .open_browser_btn
+                .connect_clicked(clone!(@weak self as obj => move |_| {
+                    gtk::show_uri(obj.transient_for().as_ref(), &uri, gdk::CURRENT_TIME);
+                }));
+
+        priv_.open_browser_btn_handler.replace(Some(handler));
+    }
+
+    pub fn connect_response<F: Fn(&Self, bool) + 'static>(&self, f: F) -> glib::SignalHandlerId {
+        self.connect_local("response", true, move |values| {
+            //FIXME The manuel cast is needed because of https://github.com/gtk-rs/gtk4-rs/issues/591
+            let obj: Self = values[0].get::<glib::Object>().unwrap().downcast().unwrap();
+            let response = values[1].get::<bool>().unwrap();
+
+            f(&obj, response);
+
+            None
+        })
+        .unwrap()
+    }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index d80ef5a3..8dea5bdd 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -1,3 +1,4 @@
+mod auth_dialog;
 mod avatar;
 mod context_menu_bin;
 mod custom_entry;
@@ -7,6 +8,7 @@ mod pill;
 mod room_title;
 mod spinner_button;
 
+pub use self::auth_dialog::{AuthData, AuthDialog};
 pub use self::avatar::Avatar;
 pub use self::context_menu_bin::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl};
 pub use self::custom_entry::CustomEntry;
diff --git a/src/meson.build b/src/meson.build
index 94e8dd32..bf83a98d 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -21,6 +21,7 @@ run_command(
 sources = files(
   'application.rs',
   'components/avatar.rs',
+  'components/auth_dialog.rs',
   'components/context_menu_bin.rs',
   'components/custom_entry.rs',
   'components/label_with_widgets.rs',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]