Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 11 additions & 0 deletions crates/handlers/src/admin/v1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ where
get_with(
self::user_registration_tokens::get,
self::user_registration_tokens::get_doc,
)
.put_with(
self::user_registration_tokens::update,
self::user_registration_tokens::update_doc,
),
)
.api_route(
Expand All @@ -145,6 +149,13 @@ where
self::user_registration_tokens::revoke_doc,
),
)
.api_route(
"/user-registration-tokens/{id}/unrevoke",
post_with(
self::user_registration_tokens::unrevoke,
self::user_registration_tokens::unrevoke_doc,
),
)
.api_route(
"/upstream-oauth-links",
get_with(
Expand Down
62 changes: 62 additions & 0 deletions crates/handlers/src/admin/v1/user_registration_tokens/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ use crate::{
#[derive(Debug, thiserror::Error, OperationIo)]
#[aide(output_with = "Json<ErrorResponse>")]
pub enum RouteError {
#[error("A registration token with the same token already exists")]
Conflict(mas_data_model::UserRegistrationToken),

#[error(transparent)]
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
}
Expand All @@ -36,6 +39,7 @@ impl IntoResponse for RouteError {
let error = ErrorResponse::from_error(&self);
let sentry_event_id = record_error!(self, Self::Internal(_));
let status = match self {
Self::Conflict(_) => StatusCode::CONFLICT,
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, sentry_event_id, Json(error)).into_response()
Expand Down Expand Up @@ -83,6 +87,12 @@ pub async fn handler(
.token
.unwrap_or_else(|| Alphanumeric.sample_string(&mut rng, 12));

// See if we have an existing token with the same token
let existing_token = repo.user_registration_token().find_by_token(&token).await?;
if let Some(existing_token) = existing_token {
return Err(RouteError::Conflict(existing_token));
}

let registration_token = repo
.user_registration_token()
.add(
Expand Down Expand Up @@ -196,4 +206,56 @@ mod tests {
}
"#);
}

#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_create_conflict(pool: PgPool) {
setup();
let mut state = TestState::from_pool(pool).await.unwrap();
let token = state.token_with_scope("urn:mas:admin").await;

let request = Request::post("/api/admin/v1/user-registration-tokens")
.bearer(&token)
.json(serde_json::json!({
"token": "test_token_123",
"usage_limit": 5
}));
let response = state.request(request).await;
response.assert_status(StatusCode::CREATED);

let body: serde_json::Value = response.json();

assert_json_snapshot!(body, @r#"
{
"data": {
"type": "user-registration_token",
"id": "01FSHN9AG0MZAA6S4AF7CTV32E",
"attributes": {
"token": "test_token_123",
"valid": true,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
}
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
}
}
"#);

let request = Request::post("/api/admin/v1/user-registration-tokens")
.bearer(&token)
.json(serde_json::json!({
"token": "test_token_123",
"usage_limit": 5
}));
let response = state.request(request).await;
response.assert_status(StatusCode::CONFLICT);
}
}
4 changes: 4 additions & 0 deletions crates/handlers/src/admin/v1/user_registration_tokens/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ mod add;
mod get;
mod list;
mod revoke;
mod unrevoke;
mod update;

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},
revoke::{doc as revoke_doc, handler as revoke},
unrevoke::{doc as unrevoke_doc, handler as unrevoke},
update::{doc as update_doc, handler as update},
};
237 changes: 237 additions & 0 deletions crates/handlers/src/admin/v1/user_registration_tokens/unrevoke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.

use aide::{OperationIo, transform::TransformOperation};
use axum::{Json, response::IntoResponse};
use hyper::StatusCode;
use mas_axum_utils::record_error;
use ulid::Ulid;

use crate::{
admin::{
call_context::CallContext,
model::{Resource, UserRegistrationToken},
params::UlidPathParam,
response::{ErrorResponse, SingleResponse},
},
impl_from_error_for_route,
};

#[derive(Debug, thiserror::Error, OperationIo)]
#[aide(output_with = "Json<ErrorResponse>")]
pub enum RouteError {
#[error(transparent)]
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),

#[error("Registration token with ID {0} not found")]
NotFound(Ulid),

#[error("Registration token with ID {0} is not revoked")]
NotRevoked(Ulid),
}

impl_from_error_for_route!(mas_storage::RepositoryError);

impl IntoResponse for RouteError {
fn into_response(self) -> axum::response::Response {
let error = ErrorResponse::from_error(&self);
let sentry_event_id = record_error!(self, Self::Internal(_));
let status = match self {
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::NotFound(_) => StatusCode::NOT_FOUND,
Self::NotRevoked(_) => StatusCode::BAD_REQUEST,
};
(status, sentry_event_id, Json(error)).into_response()
}
}

pub fn doc(operation: TransformOperation) -> TransformOperation {
operation
.id("unrevokeUserRegistrationToken")
.summary("Unrevoke a user registration token")
.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).")
.tag("user-registration-token")
.response_with::<200, Json<SingleResponse<UserRegistrationToken>>, _>(|t| {
// Get the valid token sample
let [valid_token, _] = UserRegistrationToken::samples();
let id = valid_token.id();
let response = SingleResponse::new(valid_token, format!("/api/admin/v1/user-registration-tokens/{id}/unrevoke"));
t.description("Registration token was unrevoked").example(response)
})
.response_with::<400, RouteError, _>(|t| {
let response = ErrorResponse::from_error(&RouteError::NotRevoked(Ulid::nil()));
t.description("Token is not revoked").example(response)
})
.response_with::<404, RouteError, _>(|t| {
let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
t.description("Registration token was not found").example(response)
})
}

#[tracing::instrument(name = "handler.admin.v1.user_registration_tokens.unrevoke", skip_all)]
pub async fn handler(
CallContext {
mut repo, clock, ..
}: CallContext,
id: UlidPathParam,
) -> Result<Json<SingleResponse<UserRegistrationToken>>, RouteError> {
let id = *id;
let token = repo
.user_registration_token()
.lookup(id)
.await?
.ok_or(RouteError::NotFound(id))?;

// Check if the token is not revoked
if token.revoked_at.is_none() {
return Err(RouteError::NotRevoked(id));
}

// Unrevoke the token using the repository method
let token = repo.user_registration_token().unrevoke(token).await?;

repo.save().await?;

Ok(Json(SingleResponse::new(
UserRegistrationToken::new(token, clock.now()),
format!("/api/admin/v1/user-registration-tokens/{id}/unrevoke"),
)))
}

#[cfg(test)]
mod tests {
use hyper::{Request, StatusCode};
use sqlx::PgPool;

use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};

#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_unrevoke_token(pool: PgPool) {
setup();
let mut state = TestState::from_pool(pool).await.unwrap();
let token = state.token_with_scope("urn:mas:admin").await;

let mut repo = state.repository().await.unwrap();

// Create a token
let registration_token = repo
.user_registration_token()
.add(
&mut state.rng(),
&state.clock,
"test_token_456".to_owned(),
Some(5),
None,
)
.await
.unwrap();

// Revoke it
let registration_token = repo
.user_registration_token()
.revoke(&state.clock, registration_token)
.await
.unwrap();

repo.save().await.unwrap();

// Now unrevoke it
let request = Request::post(format!(
"/api/admin/v1/user-registration-tokens/{}/unrevoke",
registration_token.id
))
.bearer(&token)
.empty();
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let body: serde_json::Value = response.json();

// The revoked_at timestamp should be null
insta::assert_json_snapshot!(body, @r#"
{
"data": {
"type": "user-registration_token",
"id": "01FSHN9AG0MZAA6S4AF7CTV32E",
"attributes": {
"token": "test_token_456",
"valid": true,
"usage_limit": 5,
"times_used": 0,
"created_at": "2022-01-16T14:40:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
}
},
"links": {
"self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E/unrevoke"
}
}
"#);
}

#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_unrevoke_not_revoked_token(pool: PgPool) {
setup();
let mut state = TestState::from_pool(pool).await.unwrap();
let token = state.token_with_scope("urn:mas:admin").await;

let mut repo = state.repository().await.unwrap();
let registration_token = repo
.user_registration_token()
.add(
&mut state.rng(),
&state.clock,
"test_token_789".to_owned(),
None,
None,
)
.await
.unwrap();

repo.save().await.unwrap();

// Try to unrevoke a token that's not revoked
let request = Request::post(format!(
"/api/admin/v1/user-registration-tokens/{}/unrevoke",
registration_token.id
))
.bearer(&token)
.empty();
let response = state.request(request).await;
response.assert_status(StatusCode::BAD_REQUEST);
let body: serde_json::Value = response.json();
assert_eq!(
body["errors"][0]["title"],
format!(
"Registration token with ID {} is not revoked",
registration_token.id
)
);
}

#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_unrevoke_unknown_token(pool: PgPool) {
setup();
let mut state = TestState::from_pool(pool).await.unwrap();
let token = state.token_with_scope("urn:mas:admin").await;

let request = Request::post(
"/api/admin/v1/user-registration-tokens/01040G2081040G2081040G2081/unrevoke",
)
.bearer(&token)
.empty();
let response = state.request(request).await;
response.assert_status(StatusCode::NOT_FOUND);
let body: serde_json::Value = response.json();
assert_eq!(
body["errors"][0]["title"],
"Registration token with ID 01040G2081040G2081040G2081 not found"
);
}
}
Loading
Loading