[fractal/fractal-next] auth-data: Add dialog to ask for authentication
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] auth-data: Add dialog to ask for authentication
- Date: Fri, 24 Sep 2021 12:47:41 +0000 (UTC)
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]