You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
admin: add API to create users
This commit is contained in:
@ -26,10 +26,12 @@ use hyper::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
|
||||
use indexmap::IndexMap;
|
||||
use mas_axum_utils::FancyError;
|
||||
use mas_http::CorsLayerExt;
|
||||
use mas_matrix::BoxHomeserverConnection;
|
||||
use mas_router::{
|
||||
ApiDoc, ApiDocCallback, OAuth2AuthorizationEndpoint, OAuth2TokenEndpoint, Route, SimpleRoute,
|
||||
UrlBuilder,
|
||||
};
|
||||
use mas_storage::BoxRng;
|
||||
use mas_templates::{ApiDocContext, Templates};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
@ -45,6 +47,8 @@ use self::call_context::CallContext;
|
||||
pub fn router<S>() -> (OpenApi, Router<S>)
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
BoxHomeserverConnection: FromRef<S>,
|
||||
BoxRng: FromRequestParts<S>,
|
||||
CallContext: FromRequestParts<S>,
|
||||
Templates: FromRef<S>,
|
||||
UrlBuilder: FromRef<S>,
|
||||
|
@ -13,7 +13,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
use aide::axum::{routing::get_with, ApiRouter};
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::extract::{FromRef, FromRequestParts};
|
||||
use mas_matrix::BoxHomeserverConnection;
|
||||
use mas_storage::BoxRng;
|
||||
|
||||
use super::call_context::CallContext;
|
||||
|
||||
@ -22,10 +24,16 @@ mod users;
|
||||
pub fn router<S>() -> ApiRouter<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
BoxHomeserverConnection: FromRef<S>,
|
||||
BoxRng: FromRequestParts<S>,
|
||||
CallContext: FromRequestParts<S>,
|
||||
{
|
||||
ApiRouter::<S>::new()
|
||||
.api_route("/users", get_with(self::users::list, self::users::list_doc))
|
||||
.api_route(
|
||||
"/users",
|
||||
get_with(self::users::list, self::users::list_doc)
|
||||
.post_with(self::users::add, self::users::add_doc),
|
||||
)
|
||||
.api_route(
|
||||
"/users/:id",
|
||||
get_with(self::users::get, self::users::get_doc),
|
||||
|
179
crates/handlers/src/admin/v1/users/add.rs
Normal file
179
crates/handlers/src/admin/v1/users/add.rs
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use aide::{transform::TransformOperation, NoApi, OperationIo};
|
||||
use axum::{extract::State, response::IntoResponse, Json};
|
||||
use hyper::StatusCode;
|
||||
use mas_matrix::BoxHomeserverConnection;
|
||||
use mas_storage::{
|
||||
job::{JobRepositoryExt, ProvisionUserJob},
|
||||
BoxRng,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
admin::{
|
||||
call_context::CallContext,
|
||||
model::User,
|
||||
response::{ErrorResponse, SingleResponse},
|
||||
},
|
||||
impl_from_error_for_route,
|
||||
};
|
||||
|
||||
fn valid_username_character(c: char) -> bool {
|
||||
c.is_ascii_lowercase()
|
||||
|| c.is_ascii_digit()
|
||||
|| c == '='
|
||||
|| c == '_'
|
||||
|| c == '-'
|
||||
|| c == '.'
|
||||
|| c == '/'
|
||||
|| c == '+'
|
||||
}
|
||||
|
||||
// XXX: this should be shared with the graphql handler
|
||||
fn username_valid(username: &str) -> bool {
|
||||
if username.is_empty() || username.len() > 255 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Should not start with an underscore
|
||||
if username.starts_with('_') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Should only contain valid characters
|
||||
if !username.chars().all(valid_username_character) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[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(transparent)]
|
||||
Homeserver(anyhow::Error),
|
||||
|
||||
#[error("Username is not valid")]
|
||||
UsernameNotValid,
|
||||
|
||||
#[error("User already exists")]
|
||||
UserAlreadyExists,
|
||||
|
||||
#[error("Username is reserved by the homeserver")]
|
||||
UsernameReserved,
|
||||
}
|
||||
|
||||
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 status = match self {
|
||||
Self::Internal(_) | Self::Homeserver(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::UsernameNotValid => StatusCode::BAD_REQUEST,
|
||||
Self::UserAlreadyExists | Self::UsernameReserved => StatusCode::CONFLICT,
|
||||
};
|
||||
(status, Json(error)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct AddUserParams {
|
||||
/// The username of the user to add.
|
||||
username: String,
|
||||
|
||||
/// Skip checking with the homeserver whether the username is available.
|
||||
///
|
||||
/// Use this with caution! The main reason to use this, is when a user used
|
||||
/// by an application service needs to exist in MAS to craft special
|
||||
/// tokens (like with admin access) for them
|
||||
#[serde(default)]
|
||||
skip_homeserver_check: bool,
|
||||
}
|
||||
|
||||
pub fn doc(operation: TransformOperation) -> TransformOperation {
|
||||
operation
|
||||
.summary("Create a new user")
|
||||
.tag("user")
|
||||
.response_with::<200, Json<SingleResponse<User>>, _>(|t| {
|
||||
let [sample, ..] = User::samples();
|
||||
let response = SingleResponse::new_canonical(sample);
|
||||
t.description("User was created").example(response)
|
||||
})
|
||||
.response_with::<400, RouteError, _>(|t| {
|
||||
let response = ErrorResponse::from_error(&RouteError::UsernameNotValid);
|
||||
t.description("Username is not valid").example(response)
|
||||
})
|
||||
.response_with::<409, RouteError, _>(|t| {
|
||||
let response = ErrorResponse::from_error(&RouteError::UserAlreadyExists);
|
||||
t.description("User already exists").example(response)
|
||||
})
|
||||
.response_with::<409, RouteError, _>(|t| {
|
||||
let response = ErrorResponse::from_error(&RouteError::UsernameReserved);
|
||||
t.description("Username is reserved by the homeserver")
|
||||
.example(response)
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "handler.admin.v1.users.add", skip_all, err)]
|
||||
pub async fn handler(
|
||||
CallContext {
|
||||
mut repo, clock, ..
|
||||
}: CallContext,
|
||||
NoApi(mut rng): NoApi<BoxRng>,
|
||||
State(homeserver): State<BoxHomeserverConnection>,
|
||||
Json(params): Json<AddUserParams>,
|
||||
) -> Result<Json<SingleResponse<User>>, RouteError> {
|
||||
if repo.user().exists(¶ms.username).await? {
|
||||
return Err(RouteError::UserAlreadyExists);
|
||||
}
|
||||
|
||||
// Do some basic check on the username
|
||||
if !username_valid(¶ms.username) {
|
||||
return Err(RouteError::UsernameNotValid);
|
||||
}
|
||||
|
||||
// Ask the homeserver if the username is available
|
||||
let homeserver_available = homeserver
|
||||
.is_localpart_available(¶ms.username)
|
||||
.await
|
||||
.map_err(RouteError::Homeserver)?;
|
||||
|
||||
if !homeserver_available {
|
||||
if !params.skip_homeserver_check {
|
||||
return Err(RouteError::UsernameReserved);
|
||||
}
|
||||
|
||||
// If we skipped the check, we still want to shout about it
|
||||
warn!("Skipped homeserver check for username {}", params.username);
|
||||
}
|
||||
|
||||
let user = repo.user().add(&mut rng, &clock, params.username).await?;
|
||||
|
||||
repo.job()
|
||||
.schedule_job(ProvisionUserJob::new(&user))
|
||||
.await?;
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
Ok(Json(SingleResponse::new_canonical(User::from(user))))
|
||||
}
|
@ -12,11 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod add;
|
||||
mod by_username;
|
||||
mod get;
|
||||
mod list;
|
||||
|
||||
pub use self::{
|
||||
add::{doc as add_doc, handler as add},
|
||||
by_username::{doc as by_username_doc, handler as by_username},
|
||||
get::{doc as get_doc, handler as get},
|
||||
list::{doc as list_doc, handler as list},
|
||||
|
@ -60,9 +60,12 @@ macro_rules! impl_from_ref {
|
||||
|
||||
impl_from_request_parts!(mas_storage::BoxRepository);
|
||||
impl_from_request_parts!(mas_storage::BoxClock);
|
||||
impl_from_request_parts!(mas_storage::BoxRng);
|
||||
impl_from_request_parts!(mas_handlers::BoundActivityTracker);
|
||||
impl_from_ref!(mas_router::UrlBuilder);
|
||||
impl_from_ref!(mas_templates::Templates);
|
||||
impl_from_ref!(mas_matrix::BoxHomeserverConnection);
|
||||
impl_from_ref!(mas_keystore::Keystore);
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (mut api, _) = mas_handlers::admin_api_router::<DummyState>();
|
||||
|
@ -349,7 +349,7 @@ fn username_valid(username: &str) -> bool {
|
||||
}
|
||||
|
||||
// Should not start with an underscore
|
||||
if username.get(0..1) == Some("_") {
|
||||
if username.starts_with('_') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user