You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-07 17:03:01 +03:00
Better check for resource ownership in the GraphQL API
This commit is contained in:
@@ -67,6 +67,49 @@ pub enum Requester {
|
|||||||
OAuth2Session(Session, User),
|
OAuth2Session(Session, User),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait OwnerId {
|
||||||
|
fn owner_id(&self) -> Option<Ulid>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwnerId for User {
|
||||||
|
fn owner_id(&self) -> Option<Ulid> {
|
||||||
|
Some(self.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwnerId for BrowserSession {
|
||||||
|
fn owner_id(&self) -> Option<Ulid> {
|
||||||
|
Some(self.user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwnerId for mas_data_model::UserEmail {
|
||||||
|
fn owner_id(&self) -> Option<Ulid> {
|
||||||
|
Some(self.user_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwnerId for mas_data_model::CompatSession {
|
||||||
|
fn owner_id(&self) -> Option<Ulid> {
|
||||||
|
Some(self.user_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwnerId for mas_data_model::UpstreamOAuthLink {
|
||||||
|
fn owner_id(&self) -> Option<Ulid> {
|
||||||
|
self.user_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dumb wrapper around a `Ulid` to implement `OwnerId` for it.
|
||||||
|
pub struct UserId(Ulid);
|
||||||
|
|
||||||
|
impl OwnerId for UserId {
|
||||||
|
fn owner_id(&self) -> Option<Ulid> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Requester {
|
impl Requester {
|
||||||
fn browser_session(&self) -> Option<&BrowserSession> {
|
fn browser_session(&self) -> Option<&BrowserSession> {
|
||||||
match self {
|
match self {
|
||||||
@@ -83,19 +126,23 @@ impl Requester {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_owner_or_admin(&self, user_id: Ulid) -> Result<(), async_graphql::Error> {
|
/// Returns true if the requester can access the resource.
|
||||||
|
fn is_owner_or_admin(&self, resource: &impl OwnerId) -> bool {
|
||||||
// If the requester is an admin, they can do anything.
|
// If the requester is an admin, they can do anything.
|
||||||
if self.is_admin() {
|
if self.is_admin() {
|
||||||
return Ok(());
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Else check that they are the owner.
|
// Otherwise, they must be the owner of the resource.
|
||||||
let user = self.user().context("Unauthorized")?;
|
let Some(owner_id) = resource.owner_id() else {
|
||||||
if user.id == user_id {
|
return false;
|
||||||
Ok(())
|
};
|
||||||
} else {
|
|
||||||
Err(async_graphql::Error::new("Unauthorized"))
|
let Some(user) = self.user() else {
|
||||||
}
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
user.id == owner_id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_admin(&self) -> bool {
|
fn is_admin(&self) -> bool {
|
||||||
|
@@ -17,6 +17,7 @@ use async_graphql::{Context, MergedObject, Object, ID};
|
|||||||
use crate::{
|
use crate::{
|
||||||
model::{Anonymous, BrowserSession, Node, NodeType, OAuth2Client, User, UserEmail},
|
model::{Anonymous, BrowserSession, Node, NodeType, OAuth2Client, User, UserEmail},
|
||||||
state::ContextExt,
|
state::ContextExt,
|
||||||
|
UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod upstream_oauth;
|
mod upstream_oauth;
|
||||||
@@ -80,24 +81,21 @@ impl BaseQuery {
|
|||||||
/// Fetch a user by its ID.
|
/// Fetch a user by its ID.
|
||||||
async fn user(&self, ctx: &Context<'_>, id: ID) -> Result<Option<User>, async_graphql::Error> {
|
async fn user(&self, ctx: &Context<'_>, id: ID) -> Result<Option<User>, async_graphql::Error> {
|
||||||
let id = NodeType::User.extract_ulid(&id)?;
|
let id = NodeType::User.extract_ulid(&id)?;
|
||||||
|
|
||||||
let requester = ctx.requester();
|
let requester = ctx.requester();
|
||||||
|
if !requester.is_owner_or_admin(&UserId(id)) {
|
||||||
let Some(current_user) = requester.user() else {
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
|
||||||
|
|
||||||
if current_user.id == id {
|
|
||||||
Ok(Some(User(current_user.clone())))
|
|
||||||
} else if requester.is_admin() {
|
|
||||||
// An admin can fetch any user, not just themselves
|
|
||||||
let state = ctx.state();
|
|
||||||
let mut repo = state.repository().await?;
|
|
||||||
let user = repo.user().lookup(id).await?;
|
|
||||||
repo.cancel().await?;
|
|
||||||
Ok(user.map(User))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We could avoid the database lookup if the requester is the user we're looking
|
||||||
|
// for but that would make the code more complex and we're not very
|
||||||
|
// concerned about performance yet
|
||||||
|
let state = ctx.state();
|
||||||
|
let mut repo = state.repository().await?;
|
||||||
|
let user = repo.user().lookup(id).await?;
|
||||||
|
repo.cancel().await?;
|
||||||
|
|
||||||
|
Ok(user.map(User))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch a browser session by its ID.
|
/// Fetch a browser session by its ID.
|
||||||
@@ -110,24 +108,19 @@ impl BaseQuery {
|
|||||||
let id = NodeType::BrowserSession.extract_ulid(&id)?;
|
let id = NodeType::BrowserSession.extract_ulid(&id)?;
|
||||||
let requester = ctx.requester();
|
let requester = ctx.requester();
|
||||||
|
|
||||||
let Some(current_user) = requester.user() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let mut repo = state.repository().await?;
|
let mut repo = state.repository().await?;
|
||||||
|
|
||||||
let browser_session = repo.browser_session().lookup(id).await?;
|
let browser_session = repo.browser_session().lookup(id).await?;
|
||||||
|
|
||||||
repo.cancel().await?;
|
repo.cancel().await?;
|
||||||
|
|
||||||
let ret = browser_session.and_then(|browser_session| {
|
let Some(browser_session) = browser_session else {
|
||||||
if browser_session.user.id == current_user.id || requester.is_admin() {
|
return Ok(None);
|
||||||
Some(BrowserSession(browser_session))
|
};
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(ret)
|
if !requester.is_owner_or_admin(&browser_session) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(BrowserSession(browser_session)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch a user email by its ID.
|
/// Fetch a user email by its ID.
|
||||||
@@ -140,20 +133,19 @@ impl BaseQuery {
|
|||||||
let id = NodeType::UserEmail.extract_ulid(&id)?;
|
let id = NodeType::UserEmail.extract_ulid(&id)?;
|
||||||
let requester = ctx.requester();
|
let requester = ctx.requester();
|
||||||
|
|
||||||
let Some(current_user) = requester.user() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let mut repo = state.repository().await?;
|
let mut repo = state.repository().await?;
|
||||||
|
let user_email = repo.user_email().lookup(id).await?;
|
||||||
let user_email = repo
|
|
||||||
.user_email()
|
|
||||||
.lookup(id)
|
|
||||||
.await?
|
|
||||||
.filter(|e| e.user_id == current_user.id || requester.is_admin());
|
|
||||||
|
|
||||||
repo.cancel().await?;
|
repo.cancel().await?;
|
||||||
|
|
||||||
Ok(user_email.map(UserEmail))
|
let Some(user_email) = user_email else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !requester.is_owner_or_admin(&user_email) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(UserEmail(user_email)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches an object given its ID.
|
/// Fetches an object given its ID.
|
||||||
|
@@ -41,18 +41,19 @@ impl UpstreamOAuthQuery {
|
|||||||
let id = NodeType::UpstreamOAuth2Link.extract_ulid(&id)?;
|
let id = NodeType::UpstreamOAuth2Link.extract_ulid(&id)?;
|
||||||
let requester = ctx.requester();
|
let requester = ctx.requester();
|
||||||
|
|
||||||
let Some(current_user) = requester.user() else {
|
let mut repo = state.repository().await?;
|
||||||
|
let link = repo.upstream_oauth_link().lookup(id).await?;
|
||||||
|
repo.cancel().await?;
|
||||||
|
|
||||||
|
let Some(link) = link else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let mut repo = state.repository().await?;
|
|
||||||
|
|
||||||
let link = repo.upstream_oauth_link().lookup(id).await?;
|
if !requester.is_owner_or_admin(&link) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure that the link belongs to the current user
|
Ok(Some(UpstreamOAuth2Link::new(link)))
|
||||||
let link =
|
|
||||||
link.filter(|link| link.user_id == Some(current_user.id) || requester.is_admin());
|
|
||||||
|
|
||||||
Ok(link.map(UpstreamOAuth2Link::new))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch an upstream OAuth 2.0 provider by its ID.
|
/// Fetch an upstream OAuth 2.0 provider by its ID.
|
||||||
@@ -68,7 +69,11 @@ impl UpstreamOAuthQuery {
|
|||||||
let provider = repo.upstream_oauth_provider().lookup(id).await?;
|
let provider = repo.upstream_oauth_provider().lookup(id).await?;
|
||||||
repo.cancel().await?;
|
repo.cancel().await?;
|
||||||
|
|
||||||
Ok(provider.map(UpstreamOAuth2Provider::new))
|
let Some(provider) = provider else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(UpstreamOAuth2Provider::new(provider)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a list of upstream OAuth 2.0 providers.
|
/// Get a list of upstream OAuth 2.0 providers.
|
||||||
|
Reference in New Issue
Block a user