mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-05-19 05:15:45 +00:00
Merge branch 'main' into rei/pat_revoke_on_deactivate
This commit is contained in:
@@ -30,6 +30,20 @@ impl User {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.locked_at.is_none() && self.deactivated_at.is_none()
|
||||
}
|
||||
|
||||
/// Returns `true` if the user is a valid actor, for example
|
||||
/// of a personal session.
|
||||
///
|
||||
/// Currently: this is `true` unless the user is deactivated.
|
||||
///
|
||||
/// This is a weaker form of validity: `is_valid` always implies
|
||||
/// `is_valid_actor`, but some users (currently: locked users)
|
||||
/// can be valid actors for personal sessions but aren't valid
|
||||
/// except through administrative access.
|
||||
#[must_use]
|
||||
pub fn is_valid_actor(&self) -> bool {
|
||||
self.deactivated_at.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
use mas_data_model::{BrowserSession, Clock, CompatSession, Session};
|
||||
use mas_data_model::{
|
||||
BrowserSession, Clock, CompatSession, Session, personal::session::PersonalSession,
|
||||
};
|
||||
|
||||
use crate::activity_tracker::ActivityTracker;
|
||||
|
||||
@@ -37,6 +39,13 @@ impl Bound {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Record activity in a personal session.
|
||||
pub async fn record_personal_session(&self, clock: &dyn Clock, session: &PersonalSession) {
|
||||
self.tracker
|
||||
.record_personal_session(clock, session, self.ip)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Record activity in a compatibility session.
|
||||
pub async fn record_compat_session(&self, clock: &dyn Clock, session: &CompatSession) {
|
||||
self.tracker
|
||||
|
||||
@@ -113,8 +113,8 @@ impl ActivityTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Record activity in a personal access token session.
|
||||
pub async fn record_personal_access_token_session(
|
||||
/// Record activity in a personal session.
|
||||
pub async fn record_personal_session(
|
||||
&self,
|
||||
clock: &dyn Clock,
|
||||
session: &PersonalSession,
|
||||
|
||||
@@ -16,8 +16,12 @@ use axum_extra::TypedHeader;
|
||||
use headers::{Authorization, authorization::Bearer};
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::record_error;
|
||||
use mas_data_model::{BoxClock, Session, User};
|
||||
use mas_data_model::{
|
||||
BoxClock, Session, TokenFormatError, TokenType, User,
|
||||
personal::session::{PersonalSession, PersonalSessionOwner},
|
||||
};
|
||||
use mas_storage::{BoxRepository, RepositoryError};
|
||||
use oauth2_types::scope::Scope;
|
||||
use ulid::Ulid;
|
||||
|
||||
use super::response::ErrorResponse;
|
||||
@@ -41,6 +45,10 @@ pub enum Rejection {
|
||||
#[error("Invalid repository operation")]
|
||||
Repository(#[from] RepositoryError),
|
||||
|
||||
/// The access token was not of the correct type for the Admin API
|
||||
#[error("Invalid type of access token")]
|
||||
InvalidAccessTokenType(#[from] Option<TokenFormatError>),
|
||||
|
||||
/// The access token could not be found in the database
|
||||
#[error("Unknown access token")]
|
||||
UnknownAccessToken,
|
||||
@@ -90,7 +98,8 @@ impl IntoResponse for Rejection {
|
||||
| Rejection::TokenExpired
|
||||
| Rejection::SessionRevoked
|
||||
| Rejection::UserLocked
|
||||
| Rejection::MissingScope => StatusCode::UNAUTHORIZED,
|
||||
| Rejection::MissingScope
|
||||
| Rejection::InvalidAccessTokenType(_) => StatusCode::UNAUTHORIZED,
|
||||
|
||||
Rejection::RepositorySetup(_)
|
||||
| Rejection::Repository(_)
|
||||
@@ -113,7 +122,7 @@ pub struct CallContext {
|
||||
pub repo: BoxRepository,
|
||||
pub clock: BoxClock,
|
||||
pub user: Option<User>,
|
||||
pub session: Session,
|
||||
pub session: CallerSession,
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for CallContext
|
||||
@@ -154,56 +163,126 @@ where
|
||||
})?;
|
||||
|
||||
let token = token.token();
|
||||
let token_type = TokenType::check(token)?;
|
||||
|
||||
// Look for the access token in the database
|
||||
let token = repo
|
||||
.oauth2_access_token()
|
||||
.find_by_token(token)
|
||||
.await?
|
||||
.ok_or(Rejection::UnknownAccessToken)?;
|
||||
let session = match token_type {
|
||||
TokenType::AccessToken => {
|
||||
// Look for the access token in the database
|
||||
let token = repo
|
||||
.oauth2_access_token()
|
||||
.find_by_token(token)
|
||||
.await?
|
||||
.ok_or(Rejection::UnknownAccessToken)?;
|
||||
|
||||
// Look for the associated session in the database
|
||||
let session = repo
|
||||
.oauth2_session()
|
||||
.lookup(token.session_id)
|
||||
.await?
|
||||
.ok_or_else(|| Rejection::LoadSession(token.session_id))?;
|
||||
// Look for the associated session in the database
|
||||
let session = repo
|
||||
.oauth2_session()
|
||||
.lookup(token.session_id)
|
||||
.await?
|
||||
.ok_or_else(|| Rejection::LoadSession(token.session_id))?;
|
||||
|
||||
// Record the activity on the session
|
||||
activity_tracker
|
||||
.record_oauth2_session(&clock, &session)
|
||||
.await;
|
||||
if !session.is_valid() {
|
||||
return Err(Rejection::SessionRevoked);
|
||||
}
|
||||
|
||||
if !token.is_valid(clock.now()) {
|
||||
return Err(Rejection::TokenExpired);
|
||||
}
|
||||
|
||||
// Record the activity on the session
|
||||
activity_tracker
|
||||
.record_oauth2_session(&clock, &session)
|
||||
.await;
|
||||
|
||||
CallerSession::OAuth2Session(session)
|
||||
}
|
||||
TokenType::PersonalAccessToken => {
|
||||
// Look for the access token in the database
|
||||
let token = repo
|
||||
.personal_access_token()
|
||||
.find_by_token(token)
|
||||
.await?
|
||||
.ok_or(Rejection::UnknownAccessToken)?;
|
||||
|
||||
// Look for the associated session in the database
|
||||
let session = repo
|
||||
.personal_session()
|
||||
.lookup(token.session_id)
|
||||
.await?
|
||||
.ok_or_else(|| Rejection::LoadSession(token.session_id))?;
|
||||
|
||||
if !session.is_valid() {
|
||||
return Err(Rejection::SessionRevoked);
|
||||
}
|
||||
|
||||
if !token.is_valid(clock.now()) {
|
||||
return Err(Rejection::TokenExpired);
|
||||
}
|
||||
|
||||
// Check the validity of the owner of the personal session
|
||||
match session.owner {
|
||||
PersonalSessionOwner::User(owner_user_id) => {
|
||||
let owner_user = repo
|
||||
.user()
|
||||
.lookup(owner_user_id)
|
||||
.await?
|
||||
.ok_or_else(|| Rejection::LoadUser(owner_user_id))?;
|
||||
if !owner_user.is_valid() {
|
||||
return Err(Rejection::UserLocked);
|
||||
}
|
||||
}
|
||||
PersonalSessionOwner::OAuth2Client(_) => {
|
||||
// nop: Client owners are always valid
|
||||
}
|
||||
}
|
||||
|
||||
// Record the activity on the session
|
||||
activity_tracker
|
||||
.record_personal_session(&clock, &session)
|
||||
.await;
|
||||
|
||||
CallerSession::PersonalSession(session)
|
||||
}
|
||||
_other => {
|
||||
return Err(Rejection::InvalidAccessTokenType(None));
|
||||
}
|
||||
};
|
||||
|
||||
// Load the user if there is one
|
||||
let user = if let Some(user_id) = session.user_id {
|
||||
let user = if let Some(user_id) = session.user_id() {
|
||||
let user = repo
|
||||
.user()
|
||||
.lookup(user_id)
|
||||
.await?
|
||||
.ok_or_else(|| Rejection::LoadUser(user_id))?;
|
||||
|
||||
match session {
|
||||
CallerSession::OAuth2Session(_) => {
|
||||
// For OAuth2 sessions: check that the user is valid enough
|
||||
// to be a user.
|
||||
if !user.is_valid() {
|
||||
return Err(Rejection::UserLocked);
|
||||
}
|
||||
}
|
||||
CallerSession::PersonalSession(_) => {
|
||||
// For personal sessions: check that the actor is valid enough
|
||||
// to be an actor.
|
||||
if !user.is_valid_actor() {
|
||||
return Err(Rejection::UserLocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(user)
|
||||
} else {
|
||||
// Double check we're not using a PersonalSession
|
||||
assert!(matches!(session, CallerSession::OAuth2Session(_)));
|
||||
None
|
||||
};
|
||||
|
||||
// If there is a user for this session, check that it is not locked
|
||||
if let Some(user) = &user
|
||||
&& !user.is_valid()
|
||||
{
|
||||
return Err(Rejection::UserLocked);
|
||||
}
|
||||
|
||||
if !session.is_valid() {
|
||||
return Err(Rejection::SessionRevoked);
|
||||
}
|
||||
|
||||
if !token.is_valid(clock.now()) {
|
||||
return Err(Rejection::TokenExpired);
|
||||
}
|
||||
|
||||
// For now, we only check that the session has the admin scope
|
||||
// Later we might want to check other route-specific scopes
|
||||
if !session.scope.contains("urn:mas:admin") {
|
||||
if !session.scope().contains("urn:mas:admin") {
|
||||
return Err(Rejection::MissingScope);
|
||||
}
|
||||
|
||||
@@ -215,3 +294,26 @@ where
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The session representing the caller of the Admin API;
|
||||
/// could either be an OAuth session or a personal session.
|
||||
pub enum CallerSession {
|
||||
OAuth2Session(Session),
|
||||
PersonalSession(PersonalSession),
|
||||
}
|
||||
|
||||
impl CallerSession {
|
||||
pub fn scope(&self) -> &Scope {
|
||||
match self {
|
||||
CallerSession::OAuth2Session(session) => &session.scope,
|
||||
CallerSession::PersonalSession(session) => &session.scope,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> Option<Ulid> {
|
||||
match self {
|
||||
CallerSession::OAuth2Session(session) => session.user_id,
|
||||
CallerSession::PersonalSession(session) => Some(session.actor_user_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum::{Json, response::IntoResponse};
|
||||
use chrono::Duration;
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::record_error;
|
||||
use mas_data_model::{BoxRng, TokenType, personal::session::PersonalSessionOwner};
|
||||
use mas_data_model::{BoxRng, TokenType};
|
||||
use oauth2_types::scope::Scope;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -19,6 +19,7 @@ use crate::{
|
||||
call_context::CallContext,
|
||||
model::{InconsistentPersonalSession, PersonalSession},
|
||||
response::{ErrorResponse, SingleResponse},
|
||||
v1::personal_sessions::personal_session_owner_from_caller,
|
||||
},
|
||||
impl_from_error_for_route,
|
||||
};
|
||||
@@ -104,13 +105,7 @@ pub async fn handler(
|
||||
NoApi(mut rng): NoApi<BoxRng>,
|
||||
Json(params): Json<Request>,
|
||||
) -> Result<(StatusCode, Json<SingleResponse<PersonalSession>>), RouteError> {
|
||||
let owner = if let Some(user_id) = session.user_id {
|
||||
// User-owned session
|
||||
PersonalSessionOwner::User(user_id)
|
||||
} else {
|
||||
// No admin user means this is a client-owned session
|
||||
PersonalSessionOwner::OAuth2Client(session.client_id)
|
||||
};
|
||||
let owner = personal_session_owner_from_caller(&session);
|
||||
|
||||
let actor_user = repo
|
||||
.user()
|
||||
|
||||
@@ -9,6 +9,8 @@ mod list;
|
||||
mod regenerate;
|
||||
mod revoke;
|
||||
|
||||
use mas_data_model::personal::session::PersonalSessionOwner;
|
||||
|
||||
pub use self::{
|
||||
add::{doc as add_doc, handler as add},
|
||||
get::{doc as get_doc, handler as get},
|
||||
@@ -16,3 +18,22 @@ pub use self::{
|
||||
regenerate::{doc as regenerate_doc, handler as regenerate},
|
||||
revoke::{doc as revoke_doc, handler as revoke},
|
||||
};
|
||||
use crate::admin::call_context::CallerSession;
|
||||
|
||||
/// Given the [`CallerSession`] of a caller of the Admin API,
|
||||
/// return the [`PersonalSessionOwner`] that should own created personal
|
||||
/// sessions.
|
||||
fn personal_session_owner_from_caller(caller: &CallerSession) -> PersonalSessionOwner {
|
||||
match caller {
|
||||
CallerSession::OAuth2Session(session) => {
|
||||
if let Some(user_id) = session.user_id {
|
||||
PersonalSessionOwner::User(user_id)
|
||||
} else {
|
||||
PersonalSessionOwner::OAuth2Client(session.client_id)
|
||||
}
|
||||
}
|
||||
CallerSession::PersonalSession(session) => {
|
||||
PersonalSessionOwner::User(session.actor_user_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum::{Json, response::IntoResponse};
|
||||
use chrono::Duration;
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::record_error;
|
||||
use mas_data_model::{BoxRng, TokenType, personal::session::PersonalSessionOwner};
|
||||
use mas_data_model::{BoxRng, TokenType};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use tracing::error;
|
||||
@@ -19,6 +19,7 @@ use crate::{
|
||||
model::{InconsistentPersonalSession, PersonalSession},
|
||||
params::UlidPathParam,
|
||||
response::{ErrorResponse, SingleResponse},
|
||||
v1::personal_sessions::personal_session_owner_from_caller,
|
||||
},
|
||||
impl_from_error_for_route,
|
||||
};
|
||||
@@ -111,11 +112,7 @@ pub async fn handler(
|
||||
|
||||
// If the owner is not the current caller, then currently we reject the
|
||||
// regeneration.
|
||||
let caller = if let Some(user_id) = caller_session.user_id {
|
||||
PersonalSessionOwner::User(user_id)
|
||||
} else {
|
||||
PersonalSessionOwner::OAuth2Client(caller_session.client_id)
|
||||
};
|
||||
let caller = personal_session_owner_from_caller(&caller_session);
|
||||
if session.owner != caller {
|
||||
return Err(RouteError::SessionNotYours);
|
||||
}
|
||||
|
||||
@@ -700,7 +700,7 @@ pub(crate) async fn post(
|
||||
};
|
||||
|
||||
activity_tracker
|
||||
.record_personal_access_token_session(&clock, &session, ip)
|
||||
.record_personal_session(&clock, &session, ip)
|
||||
.await;
|
||||
|
||||
INTROSPECTION_COUNTER.add(
|
||||
|
||||
Reference in New Issue
Block a user