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.
This commit is contained in:
Julian Sparber 2021-09-21 17:47:18 +02:00
parent 6344468b84
commit e25cb64d90
6 changed files with 521 additions and 0 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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()
}
}

View file

@ -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;

View file

@ -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',