Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion crates/handlers/src/activity_tracker/bound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions crates/handlers/src/activity_tracker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
131 changes: 100 additions & 31 deletions crates/handlers/src/admin/call_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ 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, TokenType, User, personal::session::PersonalSession};
use mas_storage::{BoxRepository, RepositoryError};
use oauth2_types::scope::Scope;
use ulid::Ulid;

use super::response::ErrorResponse;
Expand All @@ -41,6 +42,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,

/// The access token could not be found in the database
#[error("Unknown access token")]
UnknownAccessToken,
Expand Down Expand Up @@ -90,7 +95,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(_)
Expand All @@ -113,7 +119,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
Expand Down Expand Up @@ -154,28 +160,76 @@ where
})?;

let token = token.token();
let token_type = TokenType::check(token).or(Err(Rejection::InvalidAccessTokenType))?;

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))?;

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);
}

// Record the activity on the session
activity_tracker
.record_personal_session(&clock, &session)
.await;

// 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))?;

// Record the activity on the session
activity_tracker
.record_oauth2_session(&clock, &session)
.await;
CallerSession::PersonalSession(session)
}
_other => {
return Err(Rejection::InvalidAccessTokenType);
}
};

// 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)
Expand All @@ -193,17 +247,9 @@ where
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);
}

Expand All @@ -215,3 +261,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),
}
}
}
11 changes: 3 additions & 8 deletions crates/handlers/src/admin/v1/personal_sessions/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
};
Expand Down Expand Up @@ -100,13 +101,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()
Expand Down
21 changes: 21 additions & 0 deletions crates/handlers/src/admin/v1/personal_sessions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,31 @@ 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},
list::{doc as list_doc, handler as list},
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
};
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/handlers/src/oauth2/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading