1
0
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:
Quentin Gliech
2023-08-10 16:49:45 +02:00
parent 4ef3bcf336
commit ca86e5a39e
3 changed files with 101 additions and 57 deletions

View File

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

View File

@@ -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 { // We could avoid the database lookup if the requester is the user we're looking
Ok(Some(User(current_user.clone()))) // for but that would make the code more complex and we're not very
} else if requester.is_admin() { // concerned about performance yet
// An admin can fetch any user, not just themselves
let state = ctx.state(); let state = ctx.state();
let mut repo = state.repository().await?; let mut repo = state.repository().await?;
let user = repo.user().lookup(id).await?; let user = repo.user().lookup(id).await?;
repo.cancel().await?; repo.cancel().await?;
Ok(user.map(User)) Ok(user.map(User))
} else {
Ok(None)
}
} }
/// 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.

View File

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