1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-20 12:02:22 +03:00

Parse User Agents on the backend side (#2388)

* Parse user agents on the server side

* Parse and expose user agents on the backend

* Use the parsed user agent in the device consent page

* Fix the device icon tests

* Fix clippy warnings

* Box stuff to avoid large enum variants

* Ignore a clippy warning

* Fix the requester boxing
This commit is contained in:
Quentin Gliech
2024-02-23 16:47:48 +01:00
committed by GitHub
parent f171d76dc5
commit f3cbd3b315
58 changed files with 1019 additions and 855 deletions

View File

@@ -49,10 +49,10 @@ pub enum Requester {
Anonymous,
/// The requester is a browser session, stored in a cookie.
BrowserSession(BrowserSession),
BrowserSession(Box<BrowserSession>),
/// The requester is a OAuth2 session, with an access token.
OAuth2Session(Session, Option<User>),
OAuth2Session(Box<(Session, Option<User>)>),
}
trait OwnerId {
@@ -108,21 +108,21 @@ impl Requester {
fn browser_session(&self) -> Option<&BrowserSession> {
match self {
Self::BrowserSession(session) => Some(session),
Self::OAuth2Session(_, _) | Self::Anonymous => None,
Self::OAuth2Session(_) | Self::Anonymous => None,
}
}
fn user(&self) -> Option<&User> {
match self {
Self::BrowserSession(session) => Some(&session.user),
Self::OAuth2Session(_session, user) => user.as_ref(),
Self::OAuth2Session(tuple) => tuple.1.as_ref(),
Self::Anonymous => None,
}
}
fn oauth2_session(&self) -> Option<&Session> {
match self {
Self::OAuth2Session(session, _) => Some(session),
Self::OAuth2Session(tuple) => Some(&tuple.0),
Self::BrowserSession(_) | Self::Anonymous => None,
}
}
@@ -148,10 +148,10 @@ impl Requester {
fn is_admin(&self) -> bool {
match self {
Self::OAuth2Session(session, _user) => {
Self::OAuth2Session(tuple) => {
// TODO: is this the right scope?
// This has to be in sync with the policy
session.scope.contains("urn:mas:admin")
tuple.0.scope.contains("urn:mas:admin")
}
Self::BrowserSession(_) | Self::Anonymous => false,
}
@@ -160,7 +160,7 @@ impl Requester {
impl From<BrowserSession> for Requester {
fn from(session: BrowserSession) -> Self {
Self::BrowserSession(session)
Self::BrowserSession(Box::new(session))
}
}

View File

@@ -24,7 +24,7 @@ use mas_storage::{
use super::{
AppSession, CompatSession, Cursor, NodeCursor, NodeType, OAuth2Session, PreloadedTotalCount,
SessionState, User,
SessionState, User, UserAgent,
};
use crate::state::ContextExt;
@@ -87,9 +87,9 @@ impl BrowserSession {
}
}
/// The user-agent string with which the session was created.
pub async fn user_agent(&self) -> Option<&str> {
self.0.user_agent.as_deref()
/// The user-agent with which the session was created.
pub async fn user_agent(&self) -> Option<UserAgent> {
self.0.user_agent.clone().map(UserAgent::from)
}
/// The last IP address used by the session.

View File

@@ -18,7 +18,7 @@ use chrono::{DateTime, Utc};
use mas_storage::{compat::CompatSessionRepository, user::UserRepository};
use url::Url;
use super::{BrowserSession, NodeType, SessionState, User};
use super::{BrowserSession, NodeType, SessionState, User, UserAgent};
use crate::state::ContextExt;
/// Lazy-loaded reverse reference.
@@ -103,6 +103,11 @@ impl CompatSession {
self.session.finished_at()
}
/// The user-agent with which the session was created.
pub async fn user_agent(&self) -> Option<UserAgent> {
self.session.user_agent.clone().map(UserAgent::from)
}
/// The associated SSO login, if any.
pub async fn sso_login(
&self,

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use async_graphql::{Enum, Interface, Object};
use async_graphql::{Enum, Interface, Object, SimpleObject};
use chrono::{DateTime, Utc};
mod browser_sessions;
@@ -73,3 +73,69 @@ pub enum SessionState {
/// The session is no longer active.
Finished,
}
/// The type of a user agent
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum DeviceType {
/// A personal computer, laptop or desktop
Pc,
/// A mobile phone. Can also sometimes be a tablet.
Mobile,
/// A tablet
Tablet,
/// Unknown device type
Unknown,
}
impl From<mas_data_model::DeviceType> for DeviceType {
fn from(device_type: mas_data_model::DeviceType) -> Self {
match device_type {
mas_data_model::DeviceType::Pc => Self::Pc,
mas_data_model::DeviceType::Mobile => Self::Mobile,
mas_data_model::DeviceType::Tablet => Self::Tablet,
mas_data_model::DeviceType::Unknown => Self::Unknown,
}
}
}
/// A parsed user agent string
#[derive(SimpleObject)]
pub struct UserAgent {
/// The user agent string
pub raw: String,
/// The name of the browser
pub name: Option<String>,
/// The version of the browser
pub version: Option<String>,
/// The operating system name
pub os: Option<String>,
/// The operating system version
pub os_version: Option<String>,
/// The device model
pub model: Option<String>,
/// The device type
pub device_type: DeviceType,
}
impl From<mas_data_model::UserAgent> for UserAgent {
fn from(ua: mas_data_model::UserAgent) -> Self {
Self {
raw: ua.raw,
name: ua.name,
version: ua.version,
os: ua.os,
os_version: ua.os_version,
model: ua.model,
device_type: ua.device_type.into(),
}
}
}

View File

@@ -20,7 +20,7 @@ use oauth2_types::{oidc::ApplicationType, scope::Scope};
use ulid::Ulid;
use url::Url;
use super::{BrowserSession, NodeType, SessionState, User};
use super::{BrowserSession, NodeType, SessionState, User, UserAgent};
use crate::{state::ContextExt, UserId};
/// An OAuth 2.0 session represents a client session which used the OAuth APIs
@@ -67,6 +67,11 @@ impl OAuth2Session {
}
}
/// The user-agent with which the session was created.
pub async fn user_agent(&self) -> Option<UserAgent> {
self.0.user_agent.clone().map(UserAgent::from)
}
/// The state of the session.
pub async fn state(&self) -> SessionState {
match &self.0.state {

View File

@@ -40,7 +40,7 @@ pub struct EndCompatSessionInput {
/// The payload of the `endCompatSession` mutation.
pub enum EndCompatSessionPayload {
NotFound,
Ended(mas_data_model::CompatSession),
Ended(Box<mas_data_model::CompatSession>),
}
/// The status of the `endCompatSession` mutation.
@@ -66,7 +66,7 @@ impl EndCompatSessionPayload {
/// Returns the ended session.
async fn compat_session(&self) -> Option<CompatSession> {
match self {
Self::Ended(session) => Some(CompatSession::new(session.clone())),
Self::Ended(session) => Some(CompatSession::new(*session.clone())),
Self::NotFound => None,
}
}
@@ -110,6 +110,6 @@ impl CompatSessionMutations {
repo.save().await?;
Ok(EndCompatSessionPayload::Ended(session))
Ok(EndCompatSessionPayload::Ended(Box::new(session)))
}
}

View File

@@ -31,8 +31,11 @@ impl ViewerQuery {
match requester {
Requester::BrowserSession(session) => Viewer::user(session.user.clone()),
Requester::OAuth2Session(_session, Some(user)) => Viewer::user(user.clone()),
Requester::OAuth2Session(_, None) | Requester::Anonymous => Viewer::anonymous(),
Requester::OAuth2Session(tuple) => match &tuple.1 {
Some(user) => Viewer::user(user.clone()),
None => Viewer::anonymous(),
},
Requester::Anonymous => Viewer::anonymous(),
}
}
@@ -41,10 +44,8 @@ impl ViewerQuery {
let requester = ctx.requester();
match requester {
Requester::BrowserSession(session) => ViewerSession::browser_session(session.clone()),
Requester::OAuth2Session(session, _user) => {
ViewerSession::oauth2_session(session.clone())
}
Requester::BrowserSession(session) => ViewerSession::browser_session(*session.clone()),
Requester::OAuth2Session(tuple) => ViewerSession::oauth2_session(tuple.0.clone()),
Requester::Anonymous => ViewerSession::anonymous(),
}
}