1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-20 12:02:22 +03:00
Files
authentication-service/crates/graphql/src/model/users.rs

567 lines
21 KiB
Rust

// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use async_graphql::{
connection::{query, Connection, Edge, OpaqueCursor},
Context, Description, Enum, Object, ID,
};
use chrono::{DateTime, Utc};
use mas_storage::{
compat::{CompatSessionFilter, CompatSsoLoginFilter, CompatSsoLoginRepository},
oauth2::{OAuth2SessionFilter, OAuth2SessionRepository},
upstream_oauth2::{UpstreamOAuthLinkFilter, UpstreamOAuthLinkRepository},
user::{BrowserSessionFilter, BrowserSessionRepository, UserEmailFilter, UserEmailRepository},
Pagination, RepositoryAccess,
};
use super::{
browser_sessions::BrowserSessionState,
compat_sessions::{CompatSessionState, CompatSessionType, CompatSsoLogin},
matrix::MatrixUser,
oauth::OAuth2SessionState,
BrowserSession, CompatSession, Cursor, NodeCursor, NodeType, OAuth2Session,
PreloadedTotalCount, UpstreamOAuth2Link,
};
use crate::state::ContextExt;
#[derive(Description)]
/// A user is an individual's account.
pub struct User(pub mas_data_model::User);
impl From<mas_data_model::User> for User {
fn from(v: mas_data_model::User) -> Self {
Self(v)
}
}
impl From<mas_data_model::BrowserSession> for User {
fn from(v: mas_data_model::BrowserSession) -> Self {
Self(v.user)
}
}
#[Object(use_type_description)]
impl User {
/// ID of the object.
pub async fn id(&self) -> ID {
NodeType::User.id(self.0.id)
}
/// Username chosen by the user.
async fn username(&self) -> &str {
&self.0.username
}
/// When the object was created.
pub async fn created_at(&self) -> DateTime<Utc> {
self.0.created_at
}
/// When the user was locked out.
pub async fn locked_at(&self) -> Option<DateTime<Utc>> {
self.0.locked_at
}
/// Access to the user's Matrix account information.
async fn matrix(&self, ctx: &Context<'_>) -> Result<MatrixUser, async_graphql::Error> {
let state = ctx.state();
let conn = state.homeserver_connection();
Ok(MatrixUser::load(conn, &self.0.username).await?)
}
/// Primary email address of the user.
async fn primary_email(
&self,
ctx: &Context<'_>,
) -> Result<Option<UserEmail>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
let user_email = repo.user_email().get_primary(&self.0).await?.map(UserEmail);
repo.cancel().await?;
Ok(user_email)
}
/// Get the list of compatibility SSO logins, chronologically sorted
async fn compat_sso_logins(
&self,
ctx: &Context<'_>,
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
after: Option<String>,
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
before: Option<String>,
#[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
#[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
) -> Result<Connection<Cursor, CompatSsoLogin, PreloadedTotalCount>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
query(
after,
before,
first,
last,
|after, before, first, last| async move {
let after_id = after
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSsoLogin))
.transpose()?;
let before_id = before
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSsoLogin))
.transpose()?;
let pagination = Pagination::try_new(before_id, after_id, first, last)?;
let filter = CompatSsoLoginFilter::new().for_user(&self.0);
let page = repo.compat_sso_login().list(filter, pagination).await?;
// Preload the total count if requested
let count = if ctx.look_ahead().field("totalCount").exists() {
Some(repo.compat_sso_login().count(filter).await?)
} else {
None
};
repo.cancel().await?;
let mut connection = Connection::with_additional_fields(
page.has_previous_page,
page.has_next_page,
PreloadedTotalCount(count),
);
connection.edges.extend(page.edges.into_iter().map(|u| {
Edge::new(
OpaqueCursor(NodeCursor(NodeType::CompatSsoLogin, u.id)),
CompatSsoLogin(u),
)
}));
Ok::<_, async_graphql::Error>(connection)
},
)
.await
}
/// Get the list of compatibility sessions, chronologically sorted
#[allow(clippy::too_many_arguments)]
async fn compat_sessions(
&self,
ctx: &Context<'_>,
#[graphql(name = "state", desc = "List only sessions with the given state.")]
state_param: Option<CompatSessionState>,
#[graphql(name = "type", desc = "List only sessions with the given type.")]
type_param: Option<CompatSessionType>,
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
after: Option<String>,
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
before: Option<String>,
#[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
#[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
) -> Result<Connection<Cursor, CompatSession, PreloadedTotalCount>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
query(
after,
before,
first,
last,
|after, before, first, last| async move {
let after_id = after
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSession))
.transpose()?;
let before_id = before
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSession))
.transpose()?;
let pagination = Pagination::try_new(before_id, after_id, first, last)?;
// Build the query filter
let filter = CompatSessionFilter::new().for_user(&self.0);
let filter = match state_param {
Some(CompatSessionState::Active) => filter.active_only(),
Some(CompatSessionState::Finished) => filter.finished_only(),
None => filter,
};
let filter = match type_param {
Some(CompatSessionType::SsoLogin) => filter.sso_login_only(),
Some(CompatSessionType::Unknown) => filter.unknown_only(),
None => filter,
};
let page = repo.compat_session().list(filter, pagination).await?;
// Preload the total count if requested
let count = if ctx.look_ahead().field("totalCount").exists() {
Some(repo.compat_session().count(filter).await?)
} else {
None
};
repo.cancel().await?;
let mut connection = Connection::with_additional_fields(
page.has_previous_page,
page.has_next_page,
PreloadedTotalCount(count),
);
connection
.edges
.extend(page.edges.into_iter().map(|(session, sso_login)| {
Edge::new(
OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)),
CompatSession::new(session).with_loaded_sso_login(sso_login),
)
}));
Ok::<_, async_graphql::Error>(connection)
},
)
.await
}
/// Get the list of active browser sessions, chronologically sorted
async fn browser_sessions(
&self,
ctx: &Context<'_>,
#[graphql(name = "state", desc = "List only sessions in the given state.")]
state_param: Option<BrowserSessionState>,
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
after: Option<String>,
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
before: Option<String>,
#[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
#[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
) -> Result<Connection<Cursor, BrowserSession, PreloadedTotalCount>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
query(
after,
before,
first,
last,
|after, before, first, last| async move {
let after_id = after
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::BrowserSession))
.transpose()?;
let before_id = before
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::BrowserSession))
.transpose()?;
let pagination = Pagination::try_new(before_id, after_id, first, last)?;
let filter = BrowserSessionFilter::new().for_user(&self.0);
let filter = match state_param {
Some(BrowserSessionState::Active) => filter.active_only(),
Some(BrowserSessionState::Finished) => filter.finished_only(),
None => filter,
};
let page = repo.browser_session().list(filter, pagination).await?;
// Preload the total count if requested
let count = if ctx.look_ahead().field("totalCount").exists() {
Some(repo.browser_session().count(filter).await?)
} else {
None
};
repo.cancel().await?;
let mut connection = Connection::with_additional_fields(
page.has_previous_page,
page.has_next_page,
PreloadedTotalCount(count),
);
connection.edges.extend(page.edges.into_iter().map(|u| {
Edge::new(
OpaqueCursor(NodeCursor(NodeType::BrowserSession, u.id)),
BrowserSession(u),
)
}));
Ok::<_, async_graphql::Error>(connection)
},
)
.await
}
/// Get the list of emails, chronologically sorted
async fn emails(
&self,
ctx: &Context<'_>,
#[graphql(name = "state", desc = "List only emails in the given state.")]
state_param: Option<UserEmailState>,
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
after: Option<String>,
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
before: Option<String>,
#[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
#[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
) -> Result<Connection<Cursor, UserEmail, PreloadedTotalCount>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
query(
after,
before,
first,
last,
|after, before, first, last| async move {
let after_id = after
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::UserEmail))
.transpose()?;
let before_id = before
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::UserEmail))
.transpose()?;
let pagination = Pagination::try_new(before_id, after_id, first, last)?;
let filter = UserEmailFilter::new().for_user(&self.0);
let filter = match state_param {
Some(UserEmailState::Pending) => filter.pending_only(),
Some(UserEmailState::Confirmed) => filter.verified_only(),
None => filter,
};
let page = repo.user_email().list(filter, pagination).await?;
// Preload the total count if requested
let count = if ctx.look_ahead().field("totalCount").exists() {
Some(repo.user_email().count(filter).await?)
} else {
None
};
repo.cancel().await?;
let mut connection = Connection::with_additional_fields(
page.has_previous_page,
page.has_next_page,
PreloadedTotalCount(count),
);
connection.edges.extend(page.edges.into_iter().map(|u| {
Edge::new(
OpaqueCursor(NodeCursor(NodeType::UserEmail, u.id)),
UserEmail(u),
)
}));
Ok::<_, async_graphql::Error>(connection)
},
)
.await
}
/// Get the list of OAuth 2.0 sessions, chronologically sorted
#[allow(clippy::too_many_arguments)]
async fn oauth2_sessions(
&self,
ctx: &Context<'_>,
#[graphql(name = "state", desc = "List only sessions in the given state.")]
state_param: Option<OAuth2SessionState>,
#[graphql(desc = "List only sessions for the given client.")] client: Option<ID>,
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
after: Option<String>,
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
before: Option<String>,
#[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
#[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
) -> Result<Connection<Cursor, OAuth2Session, PreloadedTotalCount>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
query(
after,
before,
first,
last,
|after, before, first, last| async move {
let after_id = after
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::OAuth2Session))
.transpose()?;
let before_id = before
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::OAuth2Session))
.transpose()?;
let pagination = Pagination::try_new(before_id, after_id, first, last)?;
let client = if let Some(id) = client {
// Load the client if we're filtering by it
let id = NodeType::OAuth2Client.extract_ulid(&id)?;
let client = repo
.oauth2_client()
.lookup(id)
.await?
.ok_or(async_graphql::Error::new("Unknown client ID"))?;
Some(client)
} else {
None
};
let filter = OAuth2SessionFilter::new().for_user(&self.0);
let filter = match state_param {
Some(OAuth2SessionState::Active) => filter.active_only(),
Some(OAuth2SessionState::Finished) => filter.finished_only(),
None => filter,
};
let filter = match client.as_ref() {
Some(client) => filter.for_client(client),
None => filter,
};
let page = repo.oauth2_session().list(filter, pagination).await?;
let count = if ctx.look_ahead().field("totalCount").exists() {
Some(repo.oauth2_session().count(filter).await?)
} else {
None
};
repo.cancel().await?;
let mut connection = Connection::with_additional_fields(
page.has_previous_page,
page.has_next_page,
PreloadedTotalCount(count),
);
connection.edges.extend(page.edges.into_iter().map(|s| {
Edge::new(
OpaqueCursor(NodeCursor(NodeType::OAuth2Session, s.id)),
OAuth2Session(s),
)
}));
Ok::<_, async_graphql::Error>(connection)
},
)
.await
}
/// Get the list of upstream OAuth 2.0 links
async fn upstream_oauth2_links(
&self,
ctx: &Context<'_>,
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
after: Option<String>,
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
before: Option<String>,
#[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
#[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
) -> Result<Connection<Cursor, UpstreamOAuth2Link, PreloadedTotalCount>, async_graphql::Error>
{
let state = ctx.state();
let mut repo = state.repository().await?;
query(
after,
before,
first,
last,
|after, before, first, last| async move {
let after_id = after
.map(|x: OpaqueCursor<NodeCursor>| {
x.extract_for_type(NodeType::UpstreamOAuth2Link)
})
.transpose()?;
let before_id = before
.map(|x: OpaqueCursor<NodeCursor>| {
x.extract_for_type(NodeType::UpstreamOAuth2Link)
})
.transpose()?;
let pagination = Pagination::try_new(before_id, after_id, first, last)?;
let filter = UpstreamOAuthLinkFilter::new().for_user(&self.0);
let page = repo.upstream_oauth_link().list(filter, pagination).await?;
// Preload the total count if requested
let count = if ctx.look_ahead().field("totalCount").exists() {
Some(repo.upstream_oauth_link().count(filter).await?)
} else {
None
};
repo.cancel().await?;
let mut connection = Connection::with_additional_fields(
page.has_previous_page,
page.has_next_page,
PreloadedTotalCount(count),
);
connection.edges.extend(page.edges.into_iter().map(|s| {
Edge::new(
OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Link, s.id)),
UpstreamOAuth2Link::new(s),
)
}));
Ok::<_, async_graphql::Error>(connection)
},
)
.await
}
}
/// A user email address
#[derive(Description)]
pub struct UserEmail(pub mas_data_model::UserEmail);
#[Object(use_type_description)]
impl UserEmail {
/// ID of the object.
pub async fn id(&self) -> ID {
NodeType::UserEmail.id(self.0.id)
}
/// Email address
async fn email(&self) -> &str {
&self.0.email
}
/// When the object was created.
pub async fn created_at(&self) -> DateTime<Utc> {
self.0.created_at
}
/// When the email address was confirmed. Is `null` if the email was never
/// verified by the user.
async fn confirmed_at(&self) -> Option<DateTime<Utc>> {
self.0.confirmed_at
}
}
/// The state of a compatibility session.
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum UserEmailState {
/// The email address is pending confirmation.
Pending,
/// The email address has been confirmed.
Confirmed,
}