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:
parent
6344468b84
commit
e25cb64d90
6 changed files with 521 additions and 0 deletions
|
@ -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>
|
||||
|
|
132
data/resources/ui/components-auth-dialog.ui
Normal file
132
data/resources/ui/components-auth-dialog.ui
Normal 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>
|
||||
|
|
@ -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
|
||||
|
|
383
src/components/auth_dialog.rs
Normal file
383
src/components/auth_dialog.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue