Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit 0f2363c

Browse files
committed
Merge branch 'quenting/admin-api/sessions' into quenting/admin-api/merge
2 parents bb38025 + fec4c34 commit 0f2363c

File tree

8 files changed

+1042
-38
lines changed

8 files changed

+1042
-38
lines changed

crates/data-model/src/oauth2/session.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@ impl SessionState {
6464
Self::Finished { .. } => Err(InvalidTransitionError),
6565
}
6666
}
67+
68+
/// Returns the time the session was finished, if any
69+
///
70+
/// Returns `None` if the session is still [`Valid`].
71+
///
72+
/// [`Valid`]: SessionState::Valid
73+
#[must_use]
74+
pub fn finished_at(&self) -> Option<DateTime<Utc>> {
75+
match self {
76+
Self::Valid => None,
77+
Self::Finished { finished_at } => Some(*finished_at),
78+
}
79+
}
6780
}
6881

6982
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]

crates/handlers/src/admin/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ where
6464
.nest("/api/admin/v1", self::v1::router())
6565
.finish_api_with(&mut api, |t| {
6666
t.title("Matrix Authentication Service admin API")
67+
.tag(Tag {
68+
name: "oauth2-session".to_owned(),
69+
description: Some("Manage OAuth2 sessions".to_owned()),
70+
..Tag::default()
71+
})
6772
.tag(Tag {
6873
name: "user".to_owned(),
6974
description: Some("Manage users".to_owned()),

crates/handlers/src/admin/model.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
use std::net::IpAddr;
16+
1517
use chrono::{DateTime, Utc};
1618
use schemars::JsonSchema;
1719
use serde::Serialize;
@@ -104,3 +106,110 @@ impl Resource for User {
104106
self.id
105107
}
106108
}
109+
110+
/// A OAuth 2.0 session
111+
#[derive(Serialize, JsonSchema)]
112+
pub struct OAuth2Session {
113+
#[serde(skip)]
114+
id: Ulid,
115+
116+
/// When the object was created
117+
created_at: DateTime<Utc>,
118+
119+
/// When the session was finished
120+
finished_at: Option<DateTime<Utc>>,
121+
122+
/// The ID of the user who owns the session
123+
#[schemars(with = "Option<super::schema::Ulid>")]
124+
user_id: Option<Ulid>,
125+
126+
/// The ID of the browser session which started this session
127+
#[schemars(with = "Option<super::schema::Ulid>")]
128+
user_session_id: Option<Ulid>,
129+
130+
/// The ID of the client which requested this session
131+
#[schemars(with = "super::schema::Ulid")]
132+
client_id: Ulid,
133+
134+
/// The scope granted for this session
135+
scope: String,
136+
137+
/// The user agent string of the client which started this session
138+
user_agent: Option<String>,
139+
140+
/// The last time the session was active
141+
last_active_at: Option<DateTime<Utc>>,
142+
143+
/// The last IP address used by the session
144+
last_active_ip: Option<IpAddr>,
145+
}
146+
147+
impl From<mas_data_model::Session> for OAuth2Session {
148+
fn from(session: mas_data_model::Session) -> Self {
149+
Self {
150+
id: session.id,
151+
created_at: session.created_at,
152+
finished_at: session.finished_at(),
153+
user_id: session.user_id,
154+
user_session_id: session.user_session_id,
155+
client_id: session.client_id,
156+
scope: session.scope.to_string(),
157+
user_agent: session.user_agent.map(|ua| ua.raw),
158+
last_active_at: session.last_active_at,
159+
last_active_ip: session.last_active_ip,
160+
}
161+
}
162+
}
163+
164+
impl OAuth2Session {
165+
/// Samples of OAuth 2.0 sessions
166+
pub fn samples() -> [Self; 3] {
167+
[
168+
Self {
169+
id: Ulid::from_bytes([0x01; 16]),
170+
created_at: DateTime::default(),
171+
finished_at: None,
172+
user_id: Some(Ulid::from_bytes([0x02; 16])),
173+
user_session_id: Some(Ulid::from_bytes([0x03; 16])),
174+
client_id: Ulid::from_bytes([0x04; 16]),
175+
scope: "openid".to_owned(),
176+
user_agent: Some("Mozilla/5.0".to_owned()),
177+
last_active_at: Some(DateTime::default()),
178+
last_active_ip: Some("127.0.0.1".parse().unwrap()),
179+
},
180+
Self {
181+
id: Ulid::from_bytes([0x02; 16]),
182+
created_at: DateTime::default(),
183+
finished_at: None,
184+
user_id: None,
185+
user_session_id: None,
186+
client_id: Ulid::from_bytes([0x05; 16]),
187+
scope: "urn:mas:admin".to_owned(),
188+
user_agent: None,
189+
last_active_at: None,
190+
last_active_ip: None,
191+
},
192+
Self {
193+
id: Ulid::from_bytes([0x03; 16]),
194+
created_at: DateTime::default(),
195+
finished_at: Some(DateTime::default()),
196+
user_id: Some(Ulid::from_bytes([0x04; 16])),
197+
user_session_id: Some(Ulid::from_bytes([0x05; 16])),
198+
client_id: Ulid::from_bytes([0x06; 16]),
199+
scope: "urn:matrix:org.matrix.msc2967.client:api:*".to_owned(),
200+
user_agent: Some("Mozilla/5.0".to_owned()),
201+
last_active_at: Some(DateTime::default()),
202+
last_active_ip: Some("127.0.0.1".parse().unwrap()),
203+
},
204+
]
205+
}
206+
}
207+
208+
impl Resource for OAuth2Session {
209+
const KIND: &'static str = "oauth2-session";
210+
const PATH: &'static str = "/api/admin/v1/oauth2-sessions";
211+
212+
fn id(&self) -> Ulid {
213+
self.id
214+
}
215+
}

crates/handlers/src/admin/v1/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use mas_storage::BoxRng;
2323
use super::call_context::CallContext;
2424
use crate::passwords::PasswordManager;
2525

26+
mod oauth2_sessions;
2627
mod users;
2728

2829
pub fn router<S>() -> ApiRouter<S>
@@ -34,6 +35,14 @@ where
3435
CallContext: FromRequestParts<S>,
3536
{
3637
ApiRouter::<S>::new()
38+
.api_route(
39+
"/oauth2-sessions",
40+
get_with(self::oauth2_sessions::list, self::oauth2_sessions::list_doc),
41+
)
42+
.api_route(
43+
"/oauth2-sessions/:id",
44+
get_with(self::oauth2_sessions::get, self::oauth2_sessions::get_doc),
45+
)
3746
.api_route(
3847
"/users",
3948
get_with(self::users::list, self::users::list_doc)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2024 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use aide::{transform::TransformOperation, OperationIo};
16+
use axum::{response::IntoResponse, Json};
17+
use hyper::StatusCode;
18+
use ulid::Ulid;
19+
20+
use crate::{
21+
admin::{
22+
call_context::CallContext,
23+
model::OAuth2Session,
24+
params::UlidPathParam,
25+
response::{ErrorResponse, SingleResponse},
26+
},
27+
impl_from_error_for_route,
28+
};
29+
30+
#[derive(Debug, thiserror::Error, OperationIo)]
31+
#[aide(output_with = "Json<ErrorResponse>")]
32+
pub enum RouteError {
33+
#[error(transparent)]
34+
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
35+
36+
#[error("OAuth 2.0 session ID {0} not found")]
37+
NotFound(Ulid),
38+
}
39+
40+
impl_from_error_for_route!(mas_storage::RepositoryError);
41+
42+
impl IntoResponse for RouteError {
43+
fn into_response(self) -> axum::response::Response {
44+
let error = ErrorResponse::from_error(&self);
45+
let status = match self {
46+
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
47+
Self::NotFound(_) => StatusCode::NOT_FOUND,
48+
};
49+
(status, Json(error)).into_response()
50+
}
51+
}
52+
53+
pub fn doc(operation: TransformOperation) -> TransformOperation {
54+
operation
55+
.id("getOAuth2Session")
56+
.summary("Get an OAuth 2.0 session")
57+
.tag("oauth2-session")
58+
.response_with::<200, Json<SingleResponse<OAuth2Session>>, _>(|t| {
59+
let [sample, ..] = OAuth2Session::samples();
60+
let response = SingleResponse::new_canonical(sample);
61+
t.description("OAuth 2.0 session was found")
62+
.example(response)
63+
})
64+
.response_with::<404, RouteError, _>(|t| {
65+
let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
66+
t.description("OAuth 2.0 session was not found")
67+
.example(response)
68+
})
69+
}
70+
71+
#[tracing::instrument(name = "handler.admin.v1.oauth2_session.get", skip_all, err)]
72+
pub async fn handler(
73+
CallContext { mut repo, .. }: CallContext,
74+
id: UlidPathParam,
75+
) -> Result<Json<SingleResponse<OAuth2Session>>, RouteError> {
76+
let session = repo
77+
.oauth2_session()
78+
.lookup(*id)
79+
.await?
80+
.ok_or(RouteError::NotFound(*id))?;
81+
82+
Ok(Json(SingleResponse::new_canonical(OAuth2Session::from(
83+
session,
84+
))))
85+
}

0 commit comments

Comments
 (0)