feat(!783): Initial implementation
Adds support for extra limited-use registration tokens stored in the database, and a new service to manage them.
This commit is contained in:
parent
ecf74bb31f
commit
42f4ec34cd
9 changed files with 288 additions and 82 deletions
|
|
@ -185,7 +185,10 @@ pub(crate) async fn register_route(
|
|||
if is_guest
|
||||
&& (!services.config.allow_guest_registration
|
||||
|| (services.config.allow_registration
|
||||
&& services.globals.registration_token.is_some()))
|
||||
&& services
|
||||
.registration_tokens
|
||||
.get_config_file_token()
|
||||
.is_some()))
|
||||
{
|
||||
info!(
|
||||
"Guest registration disabled / registration enabled with token configured, \
|
||||
|
|
@ -301,7 +304,13 @@ pub(crate) async fn register_route(
|
|||
let skip_auth = body.appservice_info.is_some() || is_guest;
|
||||
|
||||
// Populate required UIAA flows
|
||||
if services.globals.registration_token.is_some() {
|
||||
if services
|
||||
.registration_tokens
|
||||
.iterate_tokens()
|
||||
.next()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
// Registration token required
|
||||
uiaainfo.flows.push(AuthFlow {
|
||||
stages: vec![AuthType::RegistrationToken],
|
||||
|
|
@ -846,19 +855,20 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
|||
|
||||
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
||||
///
|
||||
/// Checks if the provided registration token is valid at the time of checking
|
||||
///
|
||||
/// Currently does not have any ratelimiting, and this isn't very practical as
|
||||
/// there is only one registration token allowed.
|
||||
/// Checks if the provided registration token is valid at the time of checking.
|
||||
pub(crate) async fn check_registration_token_validity(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<check_registration_token_validity::v1::Request>,
|
||||
) -> Result<check_registration_token_validity::v1::Response> {
|
||||
let Some(reg_token) = services.globals.registration_token.clone() else {
|
||||
return Err!(Request(Forbidden("Server does not allow token registration")));
|
||||
};
|
||||
// TODO: ratelimit this pretty heavily
|
||||
|
||||
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
|
||||
let valid = services
|
||||
.registration_tokens
|
||||
.validate_token(&body.token)
|
||||
.await
|
||||
.is_some();
|
||||
|
||||
Ok(check_registration_token_validity::v1::Response { valid })
|
||||
}
|
||||
|
||||
/// Runs through all the deactivation steps:
|
||||
|
|
|
|||
|
|
@ -146,22 +146,6 @@ pub fn check(config: &Config) -> Result {
|
|||
));
|
||||
}
|
||||
|
||||
// check if we can read the token file path, and check if the file is empty
|
||||
if config.registration_token_file.as_ref().is_some_and(|path| {
|
||||
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
|
||||
error!("Failed to read the registration token file: {e}");
|
||||
}) else {
|
||||
return true;
|
||||
};
|
||||
|
||||
token == String::new()
|
||||
}) {
|
||||
return Err!(Config(
|
||||
"registration_token_file",
|
||||
"Registration token file was specified but is empty or failed to be read"
|
||||
));
|
||||
}
|
||||
|
||||
if config.max_request_size < 10_000_000 {
|
||||
return Err!(Config(
|
||||
"max_request_size",
|
||||
|
|
@ -190,7 +174,6 @@ pub fn check(config: &Config) -> Result {
|
|||
if config.allow_registration
|
||||
&& !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||
&& config.registration_token.is_none()
|
||||
&& config.registration_token_file.is_none()
|
||||
&& config.recaptcha_site_key.is_none()
|
||||
{
|
||||
return Err!(Config(
|
||||
|
|
@ -209,7 +192,6 @@ pub fn check(config: &Config) -> Result {
|
|||
if config.allow_registration
|
||||
&& config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||
&& config.registration_token.is_none()
|
||||
&& config.registration_token_file.is_none()
|
||||
{
|
||||
warn!(
|
||||
"Open registration is enabled via setting \
|
||||
|
|
|
|||
|
|
@ -545,7 +545,7 @@ pub struct Config {
|
|||
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
||||
///
|
||||
/// If you would like registration only via token reg, please configure
|
||||
/// `registration_token` or `registration_token_file`.
|
||||
/// `registration_token`.
|
||||
#[serde(default)]
|
||||
pub allow_registration: bool,
|
||||
|
||||
|
|
@ -583,15 +583,6 @@ pub struct Config {
|
|||
/// display: sensitive
|
||||
pub registration_token: Option<String>,
|
||||
|
||||
/// Path to a file on the system that gets read for additional registration
|
||||
/// tokens. Multiple tokens can be added if you separate them with
|
||||
/// whitespace
|
||||
///
|
||||
/// continuwuity must be able to access the file, and it must not be empty
|
||||
///
|
||||
/// example: "/etc/continuwuity/.reg_token"
|
||||
pub registration_token_file: Option<PathBuf>,
|
||||
|
||||
/// The public site key for reCaptcha. If this is provided, reCaptcha
|
||||
/// becomes required during registration. If both captcha *and*
|
||||
/// registration token are enabled, both will be required during
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ pub struct Service {
|
|||
pub server_user: OwnedUserId,
|
||||
pub admin_alias: OwnedRoomAliasId,
|
||||
pub turn_secret: String,
|
||||
pub registration_token: Option<String>,
|
||||
}
|
||||
|
||||
type RateLimitState = (Instant, u32); // Time if last failed try, number of failed tries
|
||||
|
|
@ -41,19 +40,6 @@ impl crate::Service for Service {
|
|||
},
|
||||
);
|
||||
|
||||
let registration_token = config.registration_token_file.as_ref().map_or_else(
|
||||
|| config.registration_token.clone(),
|
||||
|path| {
|
||||
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
|
||||
error!("Failed to read the registration token file: {e}");
|
||||
}) else {
|
||||
return config.registration_token.clone();
|
||||
};
|
||||
|
||||
Some(token.trim().to_owned())
|
||||
},
|
||||
);
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
db,
|
||||
server: args.server.clone(),
|
||||
|
|
@ -66,7 +52,6 @@ impl crate::Service for Service {
|
|||
)
|
||||
.expect("@conduit:server_name is valid"),
|
||||
turn_secret,
|
||||
registration_token,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ pub mod media;
|
|||
pub mod moderation;
|
||||
pub mod presence;
|
||||
pub mod pusher;
|
||||
pub mod registration_tokens;
|
||||
pub mod resolver;
|
||||
pub mod rooms;
|
||||
pub mod sending;
|
||||
|
|
|
|||
92
src/service/registration_tokens/data.rs
Normal file
92
src/service/registration_tokens/data.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
use std::{sync::Arc, time::SystemTime};
|
||||
|
||||
use conduwuit::utils::stream::{ReadyExt, TryIgnore};
|
||||
use database::{Database, Deserialized, Json, Map};
|
||||
use futures::Stream;
|
||||
use ruma::OwnedUserId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub(super) struct Data {
|
||||
registrationtoken_info: Arc<Map>,
|
||||
}
|
||||
|
||||
/// Metadata of a registration token.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DatabaseTokenInfo {
|
||||
/// The admin user who created this token.
|
||||
pub creator: OwnedUserId,
|
||||
/// The number of times this token has been used to create an account.
|
||||
pub uses: u64,
|
||||
/// When this token will expire, if it expires.
|
||||
pub expires: Option<TokenExpires>,
|
||||
}
|
||||
|
||||
impl DatabaseTokenInfo {
|
||||
pub(super) fn new(creator: OwnedUserId, expires: Option<TokenExpires>) -> Self {
|
||||
Self { creator, uses: 0, expires }
|
||||
}
|
||||
|
||||
/// Determine whether this token info represents a valid token, i.e. one
|
||||
/// that has not expired according to its [`Self::expires`] property. If
|
||||
/// [`Self::expires`] is [`None`], this function will always return `true`.
|
||||
#[must_use]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
match self.expires {
|
||||
| Some(TokenExpires::AfterUses(max_uses)) => self.uses <= max_uses,
|
||||
| Some(TokenExpires::AfterTime(expiry_time)) => {
|
||||
let now = SystemTime::now();
|
||||
|
||||
expiry_time >= now
|
||||
},
|
||||
| None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum TokenExpires {
|
||||
AfterUses(u64),
|
||||
AfterTime(SystemTime),
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub(super) fn new(db: &Arc<Database>) -> Self {
|
||||
Self {
|
||||
registrationtoken_info: db["registrationtoken_info"].clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Associate a registration token with its metadata in the database.
|
||||
pub(super) fn save_token(&self, token: &str, info: &DatabaseTokenInfo) {
|
||||
self.registrationtoken_info.put(token, Json(info));
|
||||
}
|
||||
|
||||
/// Delete a registration token.
|
||||
pub(super) fn revoke_token(&self, token: &str) { self.registrationtoken_info.del(token); }
|
||||
|
||||
/// Look up a registration token's metadata.
|
||||
pub(super) async fn lookup_token_info(&self, token: &str) -> Option<DatabaseTokenInfo> {
|
||||
self.registrationtoken_info
|
||||
.qry(token)
|
||||
.await
|
||||
.deserialized()
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Iterate over all valid tokens and delete expired ones.
|
||||
pub(super) fn iterate_and_clean_tokens(
|
||||
&self,
|
||||
) -> impl Stream<Item = (&str, DatabaseTokenInfo)> + Send + '_ {
|
||||
self.registrationtoken_info
|
||||
.stream()
|
||||
.ignore_err()
|
||||
.ready_filter(|item: &(&str, DatabaseTokenInfo)| {
|
||||
if item.1.is_valid() {
|
||||
true
|
||||
} else {
|
||||
self.registrationtoken_info.del(item.0);
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
152
src/service/registration_tokens/mod.rs
Normal file
152
src/service/registration_tokens/mod.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
mod data;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use conduwuit::{Err, Result, utils};
|
||||
use data::Data;
|
||||
pub use data::{DatabaseTokenInfo, TokenExpires};
|
||||
use futures::{Stream, StreamExt, stream};
|
||||
use ruma::OwnedUserId;
|
||||
|
||||
use crate::{Dep, config};
|
||||
|
||||
const RANDOM_TOKEN_LENGTH: usize = 64;
|
||||
|
||||
pub struct Service {
|
||||
db: Data,
|
||||
services: Services,
|
||||
}
|
||||
|
||||
struct Services {
|
||||
config: Dep<config::Service>,
|
||||
}
|
||||
|
||||
/// A validated registration token which may be used to create an account.
|
||||
pub struct ValidToken<'token> {
|
||||
pub token: &'token str,
|
||||
pub source: ValidTokenSource,
|
||||
}
|
||||
|
||||
impl PartialEq<str> for ValidToken<'_> {
|
||||
fn eq(&self, other: &str) -> bool { self.token == other }
|
||||
}
|
||||
|
||||
/// The source of a valid database token.
|
||||
pub enum ValidTokenSource {
|
||||
/// The static token set in the homeserver's config file, which is
|
||||
/// always valid.
|
||||
ConfigFile,
|
||||
/// A database token which has been checked to be valid.
|
||||
Database(DatabaseTokenInfo),
|
||||
}
|
||||
|
||||
impl crate::Service for Service {
|
||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
Ok(Arc::new(Self {
|
||||
db: Data::new(args.db),
|
||||
services: Services {
|
||||
config: args.depend::<config::Service>("config"),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||
}
|
||||
|
||||
impl Service {
|
||||
/// Issue a new registration token and save it in the database.
|
||||
pub fn issue_token(
|
||||
&self,
|
||||
creator: OwnedUserId,
|
||||
expires: Option<TokenExpires>,
|
||||
) -> (String, DatabaseTokenInfo) {
|
||||
let token = utils::random_string(RANDOM_TOKEN_LENGTH);
|
||||
let info = DatabaseTokenInfo::new(creator, expires);
|
||||
|
||||
self.db.save_token(&token, &info);
|
||||
(token, info)
|
||||
}
|
||||
|
||||
/// Get the registration token set in the config file, if it exists.
|
||||
pub fn get_config_file_token(&self) -> Option<ValidToken<'_>> {
|
||||
self.services
|
||||
.config
|
||||
.registration_token
|
||||
.as_deref()
|
||||
.map(|token| ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::ConfigFile,
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate a registration token.
|
||||
pub async fn validate_token<'token>(&self, token: &'token str) -> Option<ValidToken<'token>> {
|
||||
// Check the registration token in the config first
|
||||
if self
|
||||
.get_config_file_token()
|
||||
.is_some_and(|valid_token| valid_token == *token)
|
||||
{
|
||||
return Some(ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::ConfigFile,
|
||||
});
|
||||
}
|
||||
|
||||
// Now check the database
|
||||
if let Some(token_info) = self.db.lookup_token_info(token).await
|
||||
&& token_info.is_valid()
|
||||
{
|
||||
return Some(ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::Database(token_info),
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise it's not valid
|
||||
None
|
||||
}
|
||||
|
||||
/// Mark a valid token as having been used to create a new account.
|
||||
pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken<'_>) {
|
||||
match source {
|
||||
| ValidTokenSource::ConfigFile => {
|
||||
// we don't track uses of the config file token, do nothing
|
||||
},
|
||||
| ValidTokenSource::Database(mut info) => {
|
||||
info.uses = info.uses.saturating_add(1);
|
||||
|
||||
self.db.save_token(token, &info);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to revoke a valid token.
|
||||
///
|
||||
/// Note that some tokens (like the one set in the config file) cannot be
|
||||
/// revoked.
|
||||
pub fn revoke_token(&self, ValidToken { token, source }: ValidToken<'_>) -> Result {
|
||||
match source {
|
||||
| ValidTokenSource::ConfigFile => {
|
||||
// the config file token cannot be revoked
|
||||
Err!(
|
||||
"The token set in the config file cannot be revoked. Edit the config file \
|
||||
to change it."
|
||||
)
|
||||
},
|
||||
| ValidTokenSource::Database(_) => {
|
||||
self.db.revoke_token(token);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all valid registration tokens.
|
||||
pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken<'_>> + Send + '_ {
|
||||
stream::iter(self.get_config_file_token()).chain(self.db.iterate_and_clean_tokens().map(
|
||||
|(token, info)| ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::Database(info),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,9 @@ use crate::{
|
|||
account_data, admin, announcements, antispam, appservice, client, config, emergency,
|
||||
federation, globals, key_backups,
|
||||
manager::Manager,
|
||||
media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
|
||||
service::{Args, Map, Service},
|
||||
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending,
|
||||
server_keys,
|
||||
service::{self, Args, Map, Service},
|
||||
sync, transaction_ids, uiaa, users,
|
||||
};
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ pub struct Services {
|
|||
pub media: Arc<media::Service>,
|
||||
pub presence: Arc<presence::Service>,
|
||||
pub pusher: Arc<pusher::Service>,
|
||||
pub registration_tokens: Arc<registration_tokens::Service>,
|
||||
pub resolver: Arc<resolver::Service>,
|
||||
pub rooms: rooms::Service,
|
||||
pub federation: Arc<federation::Service>,
|
||||
|
|
@ -77,6 +79,7 @@ impl Services {
|
|||
media: build!(media::Service),
|
||||
presence: build!(presence::Service),
|
||||
pusher: build!(pusher::Service),
|
||||
registration_tokens: build!(registration_tokens::Service),
|
||||
rooms: rooms::Service {
|
||||
alias: build!(rooms::alias::Service),
|
||||
auth_chain: build!(rooms::auth_chain::Service),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use conduwuit::{
|
||||
Err, Error, Result, SyncRwLock, err, error, implement, utils,
|
||||
|
|
@ -16,7 +13,7 @@ use ruma::{
|
|||
},
|
||||
};
|
||||
|
||||
use crate::{Dep, config, globals, users};
|
||||
use crate::{Dep, config, globals, registration_tokens, users};
|
||||
|
||||
pub struct Service {
|
||||
userdevicesessionid_uiaarequest: SyncRwLock<RequestMap>,
|
||||
|
|
@ -28,6 +25,7 @@ struct Services {
|
|||
globals: Dep<globals::Service>,
|
||||
users: Dep<users::Service>,
|
||||
config: Dep<config::Service>,
|
||||
registration_tokens: Dep<registration_tokens::Service>,
|
||||
}
|
||||
|
||||
struct Data {
|
||||
|
|
@ -50,6 +48,8 @@ impl crate::Service for Service {
|
|||
globals: args.depend::<globals::Service>("globals"),
|
||||
users: args.depend::<users::Service>("users"),
|
||||
config: args.depend::<config::Service>("config"),
|
||||
registration_tokens: args
|
||||
.depend::<registration_tokens::Service>("registration_tokens"),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
|
@ -57,26 +57,6 @@ impl crate::Service for Service {
|
|||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||
}
|
||||
|
||||
#[implement(Service)]
|
||||
pub async fn read_tokens(&self) -> Result<HashSet<String>> {
|
||||
let mut tokens = HashSet::new();
|
||||
if let Some(file) = &self.services.config.registration_token_file.as_ref() {
|
||||
match std::fs::read_to_string(file) {
|
||||
| Ok(text) => {
|
||||
text.split_ascii_whitespace().for_each(|token| {
|
||||
tokens.insert(token.to_owned());
|
||||
});
|
||||
},
|
||||
| Err(e) => error!("Failed to read the registration token file: {e}"),
|
||||
}
|
||||
}
|
||||
if let Some(token) = &self.services.config.registration_token {
|
||||
tokens.insert(token.to_owned());
|
||||
}
|
||||
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
/// Creates a new Uiaa session. Make sure the session token is unique.
|
||||
#[implement(Service)]
|
||||
pub fn create(
|
||||
|
|
@ -229,8 +209,18 @@ pub async fn try_auth(
|
|||
}
|
||||
},
|
||||
| AuthData::RegistrationToken(t) => {
|
||||
let tokens = self.read_tokens().await?;
|
||||
if tokens.contains(t.token.trim()) {
|
||||
let token = t.token.trim();
|
||||
|
||||
if let Some(valid_token) = self
|
||||
.services
|
||||
.registration_tokens
|
||||
.validate_token(token)
|
||||
.await
|
||||
{
|
||||
self.services
|
||||
.registration_tokens
|
||||
.mark_token_as_used(valid_token);
|
||||
|
||||
uiaainfo.completed.push(AuthType::RegistrationToken);
|
||||
} else {
|
||||
uiaainfo.auth_error = Some(StandardErrorBody {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue