Skip to content

Commit 09d3bbc

Browse files
committed
Admin API to un-revoke a user registration token.
1 parent c595a0d commit 09d3bbc

File tree

7 files changed

+425
-0
lines changed

7 files changed

+425
-0
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ where
145145
self::user_registration_tokens::revoke_doc,
146146
),
147147
)
148+
.api_route(
149+
"/user-registration-tokens/{id}/unrevoke",
150+
post_with(
151+
self::user_registration_tokens::unrevoke,
152+
self::user_registration_tokens::unrevoke_doc,
153+
),
154+
)
148155
.api_route(
149156
"/upstream-oauth-links",
150157
get_with(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ mod add;
77
mod get;
88
mod list;
99
mod revoke;
10+
mod unrevoke;
1011

1112
pub use self::{
1213
add::{doc as add_doc, handler as add},
1314
get::{doc as get_doc, handler as get},
1415
list::{doc as list_doc, handler as list},
1516
revoke::{doc as revoke_doc, handler as revoke},
17+
unrevoke::{doc as unrevoke_doc, handler as unrevoke},
1618
};
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2025 The Matrix.org Foundation C.I.C.
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-only
4+
// Please see LICENSE in the repository root for full details.
5+
6+
use aide::{OperationIo, transform::TransformOperation};
7+
use axum::{Json, response::IntoResponse};
8+
use hyper::StatusCode;
9+
use mas_axum_utils::record_error;
10+
use ulid::Ulid;
11+
12+
use crate::{
13+
admin::{
14+
call_context::CallContext,
15+
model::{Resource, UserRegistrationToken},
16+
params::UlidPathParam,
17+
response::{ErrorResponse, SingleResponse},
18+
},
19+
impl_from_error_for_route,
20+
};
21+
22+
#[derive(Debug, thiserror::Error, OperationIo)]
23+
#[aide(output_with = "Json<ErrorResponse>")]
24+
pub enum RouteError {
25+
#[error(transparent)]
26+
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
27+
28+
#[error("Registration token with ID {0} not found")]
29+
NotFound(Ulid),
30+
31+
#[error("Registration token with ID {0} is not revoked")]
32+
NotRevoked(Ulid),
33+
}
34+
35+
impl_from_error_for_route!(mas_storage::RepositoryError);
36+
37+
impl IntoResponse for RouteError {
38+
fn into_response(self) -> axum::response::Response {
39+
let error = ErrorResponse::from_error(&self);
40+
let sentry_event_id = record_error!(self, Self::Internal(_));
41+
let status = match self {
42+
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
43+
Self::NotFound(_) => StatusCode::NOT_FOUND,
44+
Self::NotRevoked(_) => StatusCode::BAD_REQUEST,
45+
};
46+
(status, sentry_event_id, Json(error)).into_response()
47+
}
48+
}
49+
50+
pub fn doc(operation: TransformOperation) -> TransformOperation {
51+
operation
52+
.id("unrevokeUserRegistrationToken")
53+
.summary("Unrevoke a user registration token")
54+
.description("Calling this endpoint will unrevoke a previously revoked user registration token, allowing it to be used for registrations again (subject to its usage limits and expiration).")
55+
.tag("user-registration-token")
56+
.response_with::<200, Json<SingleResponse<UserRegistrationToken>>, _>(|t| {
57+
// Get the valid token sample
58+
let [valid_token, _] = UserRegistrationToken::samples();
59+
let id = valid_token.id();
60+
let response = SingleResponse::new(valid_token, format!("/api/admin/v1/user-registration-tokens/{id}/unrevoke"));
61+
t.description("Registration token was unrevoked").example(response)
62+
})
63+
.response_with::<400, RouteError, _>(|t| {
64+
let response = ErrorResponse::from_error(&RouteError::NotRevoked(Ulid::nil()));
65+
t.description("Token is not revoked").example(response)
66+
})
67+
.response_with::<404, RouteError, _>(|t| {
68+
let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
69+
t.description("Registration token was not found").example(response)
70+
})
71+
}
72+
73+
#[tracing::instrument(name = "handler.admin.v1.user_registration_tokens.unrevoke", skip_all)]
74+
pub async fn handler(
75+
CallContext {
76+
mut repo, clock, ..
77+
}: CallContext,
78+
id: UlidPathParam,
79+
) -> Result<Json<SingleResponse<UserRegistrationToken>>, RouteError> {
80+
let id = *id;
81+
let token = repo
82+
.user_registration_token()
83+
.lookup(id)
84+
.await?
85+
.ok_or(RouteError::NotFound(id))?;
86+
87+
// Check if the token is not revoked
88+
if token.revoked_at.is_none() {
89+
return Err(RouteError::NotRevoked(id));
90+
}
91+
92+
// Unrevoke the token using the repository method
93+
let token = repo.user_registration_token().unrevoke(token).await?;
94+
95+
repo.save().await?;
96+
97+
Ok(Json(SingleResponse::new(
98+
UserRegistrationToken::new(token, clock.now()),
99+
format!("/api/admin/v1/user-registration-tokens/{id}/unrevoke"),
100+
)))
101+
}
102+
103+
#[cfg(test)]
104+
mod tests {
105+
use hyper::{Request, StatusCode};
106+
use sqlx::PgPool;
107+
108+
use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
109+
110+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
111+
async fn test_unrevoke_token(pool: PgPool) {
112+
setup();
113+
let mut state = TestState::from_pool(pool).await.unwrap();
114+
let token = state.token_with_scope("urn:mas:admin").await;
115+
116+
let mut repo = state.repository().await.unwrap();
117+
118+
// Create a token
119+
let registration_token = repo
120+
.user_registration_token()
121+
.add(
122+
&mut state.rng(),
123+
&state.clock,
124+
"test_token_456".to_owned(),
125+
Some(5),
126+
None,
127+
)
128+
.await
129+
.unwrap();
130+
131+
// Revoke it
132+
let registration_token = repo
133+
.user_registration_token()
134+
.revoke(&state.clock, registration_token)
135+
.await
136+
.unwrap();
137+
138+
repo.save().await.unwrap();
139+
140+
// Now unrevoke it
141+
let request = Request::post(format!(
142+
"/api/admin/v1/user-registration-tokens/{}/unrevoke",
143+
registration_token.id
144+
))
145+
.bearer(&token)
146+
.empty();
147+
let response = state.request(request).await;
148+
response.assert_status(StatusCode::OK);
149+
let body: serde_json::Value = response.json();
150+
151+
// The revoked_at timestamp should be null
152+
assert_eq!(
153+
body["data"]["attributes"]["revoked_at"],
154+
serde_json::json!(null)
155+
);
156+
}
157+
158+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
159+
async fn test_unrevoke_not_revoked_token(pool: PgPool) {
160+
setup();
161+
let mut state = TestState::from_pool(pool).await.unwrap();
162+
let token = state.token_with_scope("urn:mas:admin").await;
163+
164+
let mut repo = state.repository().await.unwrap();
165+
let registration_token = repo
166+
.user_registration_token()
167+
.add(
168+
&mut state.rng(),
169+
&state.clock,
170+
"test_token_789".to_owned(),
171+
None,
172+
None,
173+
)
174+
.await
175+
.unwrap();
176+
177+
repo.save().await.unwrap();
178+
179+
// Try to unrevoke a token that's not revoked
180+
let request = Request::post(format!(
181+
"/api/admin/v1/user-registration-tokens/{}/unrevoke",
182+
registration_token.id
183+
))
184+
.bearer(&token)
185+
.empty();
186+
let response = state.request(request).await;
187+
response.assert_status(StatusCode::BAD_REQUEST);
188+
let body: serde_json::Value = response.json();
189+
assert_eq!(
190+
body["errors"][0]["title"],
191+
format!(
192+
"Registration token with ID {} is not revoked",
193+
registration_token.id
194+
)
195+
);
196+
}
197+
198+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
199+
async fn test_unrevoke_unknown_token(pool: PgPool) {
200+
setup();
201+
let mut state = TestState::from_pool(pool).await.unwrap();
202+
let token = state.token_with_scope("urn:mas:admin").await;
203+
204+
let request = Request::post(
205+
"/api/admin/v1/user-registration-tokens/01040G2081040G2081040G2081/unrevoke",
206+
)
207+
.bearer(&token)
208+
.empty();
209+
let response = state.request(request).await;
210+
response.assert_status(StatusCode::NOT_FOUND);
211+
let body: serde_json::Value = response.json();
212+
assert_eq!(
213+
body["errors"][0]["title"],
214+
"Registration token with ID 01040G2081040G2081040G2081 not found"
215+
);
216+
}
217+
}

crates/storage-pg/.sqlx/query-3c7fc3e386ce51187f6344ad65e1d78a7f026e8311bdc7d5ccc2f39d962e898f.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/storage-pg/src/user/registration_token.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,38 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> {
546546

547547
Ok(token)
548548
}
549+
550+
#[tracing::instrument(
551+
name = "db.user_registration_token.unrevoke",
552+
skip_all,
553+
fields(
554+
db.query.text,
555+
user_registration_token.id = %token.id,
556+
),
557+
err,
558+
)]
559+
async fn unrevoke(
560+
&mut self,
561+
mut token: UserRegistrationToken,
562+
) -> Result<UserRegistrationToken, Self::Error> {
563+
let res = sqlx::query!(
564+
r#"
565+
UPDATE user_registration_tokens
566+
SET revoked_at = NULL
567+
WHERE user_registration_token_id = $1
568+
"#,
569+
Uuid::from(token.id),
570+
)
571+
.traced()
572+
.execute(&mut *self.conn)
573+
.await?;
574+
575+
DatabaseError::ensure_affected_rows(&res, 1)?;
576+
577+
token.revoked_at = None;
578+
579+
Ok(token)
580+
}
549581
}
550582

551583
#[cfg(test)]
@@ -560,6 +592,51 @@ mod tests {
560592

561593
use crate::PgRepository;
562594

595+
#[sqlx::test(migrator = "crate::MIGRATOR")]
596+
async fn test_unrevoke(pool: PgPool) {
597+
let mut rng = ChaChaRng::seed_from_u64(42);
598+
let clock = MockClock::default();
599+
600+
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
601+
602+
// Create a token
603+
let token = repo
604+
.user_registration_token()
605+
.add(&mut rng, &clock, "test_token".to_owned(), None, None)
606+
.await
607+
.unwrap();
608+
609+
// Revoke the token
610+
let revoked_token = repo
611+
.user_registration_token()
612+
.revoke(&clock, token)
613+
.await
614+
.unwrap();
615+
616+
// Verify it's revoked
617+
assert!(revoked_token.revoked_at.is_some());
618+
619+
// Unrevoke the token
620+
let unrevoked_token = repo
621+
.user_registration_token()
622+
.unrevoke(revoked_token)
623+
.await
624+
.unwrap();
625+
626+
// Verify it's no longer revoked
627+
assert!(unrevoked_token.revoked_at.is_none());
628+
629+
// Check that we can find it with the non-revoked filter
630+
let non_revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(false);
631+
let page = repo
632+
.user_registration_token()
633+
.list(non_revoked_filter, Pagination::first(10))
634+
.await
635+
.unwrap();
636+
637+
assert!(page.edges.iter().any(|t| t.id == unrevoked_token.id));
638+
}
639+
563640
#[sqlx::test(migrator = "crate::MIGRATOR")]
564641
async fn test_list_and_count(pool: PgPool) {
565642
let mut rng = ChaChaRng::seed_from_u64(42);

crates/storage/src/user/registration_token.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,20 @@ pub trait UserRegistrationTokenRepository: Send + Sync {
196196
token: UserRegistrationToken,
197197
) -> Result<UserRegistrationToken, Self::Error>;
198198

199+
/// Unrevoke a previously revoked [`UserRegistrationToken`]
200+
///
201+
/// # Parameters
202+
///
203+
/// * `token`: The [`UserRegistrationToken`] to unrevoke
204+
///
205+
/// # Errors
206+
///
207+
/// Returns [`Self::Error`] if the underlying repository fails
208+
async fn unrevoke(
209+
&mut self,
210+
token: UserRegistrationToken,
211+
) -> Result<UserRegistrationToken, Self::Error>;
212+
199213
/// List [`UserRegistrationToken`]s based on the provided filter
200214
///
201215
/// Returns a list of matching [`UserRegistrationToken`]s
@@ -249,6 +263,10 @@ repository_impl!(UserRegistrationTokenRepository:
249263
clock: &dyn Clock,
250264
token: UserRegistrationToken,
251265
) -> Result<UserRegistrationToken, Self::Error>;
266+
async fn unrevoke(
267+
&mut self,
268+
token: UserRegistrationToken,
269+
) -> Result<UserRegistrationToken, Self::Error>;
252270
async fn list(
253271
&mut self,
254272
filter: UserRegistrationTokenFilter,

0 commit comments

Comments
 (0)