diff --git a/docs/src/config.md b/docs/src/config.md
index c2dfad5c6e..127652b6d9 100644
--- a/docs/src/config.md
+++ b/docs/src/config.md
@@ -106,6 +106,7 @@ The following configuration options are available.
| SYNC_TOKENSERVER__STATSD_LABEL | syncstorage.tokenserver | StatsD metrics label prefix |
| SYNC_TOKENSERVER__TOKEN_DURATION | 3600 | Token TTL (1 hour) |
| SYNC_TOKENSERVER__FXA_WEBHOOK_ENABLED | false | Enable the FxA webhook endpoint. When disabled, the route is not registered. |
+| SYNC_TOKENSERVER__ALLOW_NEW_USERS | true | Whether new users may be created. When disabled, only previously registered users can use the tokenserver service. |
| SYNC_TOKENSERVER__FXA_WEBHOOK_METRICS_ONLY | false | Run the FxA webhook handler in metrics-only mode. Received events are counted but not processed. Only used if `FXA_WEBHOOK_ENABLED` is true. |
| SYNC_TOKENSERVER__FXA_WEBHOOK_SET_CLIENT_ID | None | Expected `aud` of FxA Security Event Tokens. Required for account event webhooks. |
| SYNC_TOKENSERVER__FXA_WEBHOOK_SET_ISSUER | None | Expected `iss` of FxA Security Event Tokens. Required for account event webhooks. |
diff --git a/syncserver/src/tokenserver/extractors.rs b/syncserver/src/tokenserver/extractors.rs
index 4fe950d784..4b99dbeedf 100644
--- a/syncserver/src/tokenserver/extractors.rs
+++ b/syncserver/src/tokenserver/extractors.rs
@@ -249,6 +249,7 @@ impl FromRequest for TokenserverRequest {
client_state: auth_data.client_state.clone(),
keys_changed_at: auth_data.keys_changed_at,
capacity_release_rate: state.node_capacity_release_rate,
+ allow_new_users: state.allow_new_users,
})
.await?;
log_items_mutator.insert("first_seen_at".to_owned(), user.first_seen_at.to_string());
@@ -1345,6 +1346,7 @@ mod tests {
token_duration: TOKEN_DURATION,
set_verifiers: Vec::new(),
fxa_webhook_enabled: false,
+ allow_new_users: true,
fxa_webhook_metrics_only: false,
}
}
@@ -1368,6 +1370,7 @@ mod tests {
token_duration: TOKEN_DURATION,
set_verifiers,
fxa_webhook_enabled: true,
+ allow_new_users: true,
fxa_webhook_metrics_only: false,
}
}
diff --git a/syncserver/src/tokenserver/handlers.rs b/syncserver/src/tokenserver/handlers.rs
index 8d03ddbd8c..824a134c8b 100644
--- a/syncserver/src/tokenserver/handlers.rs
+++ b/syncserver/src/tokenserver/handlers.rs
@@ -461,6 +461,7 @@ mod tests {
token_duration: 3600,
set_verifiers,
fxa_webhook_enabled: true,
+ allow_new_users: true,
fxa_webhook_metrics_only: false,
}
}
diff --git a/syncserver/src/tokenserver/mod.rs b/syncserver/src/tokenserver/mod.rs
index ab45b852bc..32aae68a5a 100644
--- a/syncserver/src/tokenserver/mod.rs
+++ b/syncserver/src/tokenserver/mod.rs
@@ -34,6 +34,7 @@ pub struct ServerState {
pub token_duration: u64,
pub set_verifiers: Vec,
pub fxa_webhook_enabled: bool,
+ pub allow_new_users: bool,
pub fxa_webhook_metrics_only: bool,
}
@@ -111,6 +112,7 @@ impl ServerState {
token_duration: settings.token_duration,
set_verifiers,
fxa_webhook_enabled: settings.fxa_webhook_enabled,
+ allow_new_users: settings.allow_new_users,
fxa_webhook_metrics_only: settings.fxa_webhook_metrics_only,
})
}
diff --git a/tokenserver-db-common/src/error.rs b/tokenserver-db-common/src/error.rs
index 50343a328b..1d6bb84f50 100644
--- a/tokenserver-db-common/src/error.rs
+++ b/tokenserver-db-common/src/error.rs
@@ -18,6 +18,10 @@ pub struct DbError {
}
impl DbError {
+ pub fn is_user_not_created(user: String) -> Self {
+ DbErrorKind::UserNotCreated(user).into()
+ }
+
pub fn internal(msg: String) -> Self {
DbErrorKind::Internal(msg).into()
}
@@ -26,6 +30,11 @@ impl DbError {
DbErrorKind::PoolTimeout(timeout_type).into()
}
+ #[cfg(debug_assertions)]
+ pub fn is_user_not_created(&self) -> bool {
+ matches!(&self.kind, DbErrorKind::UserNotCreated(_))
+ }
+
#[cfg(debug_assertions)]
pub fn is_diesel_not_found(&self) -> bool {
matches!(&self.kind, DbErrorKind::Sql(e) if e.is_diesel_not_found())
@@ -59,6 +68,9 @@ impl ReportableError for DbError {
#[derive(Debug, Error)]
enum DbErrorKind {
+ #[error("Specified user does not exist (new users are disallowed): {}", _0)]
+ UserNotCreated(String),
+
#[error("{}", _0)]
Sql(SqlError),
@@ -72,6 +84,11 @@ enum DbErrorKind {
impl From for DbError {
fn from(kind: DbErrorKind) -> Self {
match kind {
+ DbErrorKind::UserNotCreated(_) => Self {
+ kind,
+ status: StatusCode::FORBIDDEN,
+ backtrace: Box::new(Backtrace::new_unresolved()),
+ },
DbErrorKind::Sql(ref sqle) => Self {
status: sqle.status,
backtrace: Box::new(sqle.backtrace.clone()),
diff --git a/tokenserver-db-common/src/lib.rs b/tokenserver-db-common/src/lib.rs
index 50cccc3bb4..9a4afba379 100644
--- a/tokenserver-db-common/src/lib.rs
+++ b/tokenserver-db-common/src/lib.rs
@@ -113,7 +113,7 @@ pub trait Db {
fn metrics(&self) -> &Metrics;
/// Gets the user with the given email and service ID.
- /// If one doesn't exist, allocates a new user.
+ /// If one doesn't exist, and when configuration allows it, allocates a new user.
async fn get_or_create_user(
&mut self,
params: params::GetOrCreateUser,
@@ -127,7 +127,11 @@ pub trait Db {
if raw_users.is_empty() {
// There are no users in the database with the given email and service ID, so
- // allocate a new one.
+ // allocate a new one if allowed by configuration.
+ if !params.allow_new_users {
+ return Err(DbError::user_not_created(params.email.clone()));
+ }
+
let allocate_user_result = self
.allocate_user(params.clone() as params::AllocateUser)
.await?;
@@ -193,6 +197,7 @@ pub trait Db {
client_state: raw_user.client_state.clone(),
keys_changed_at: raw_user.keys_changed_at,
capacity_release_rate: params.capacity_release_rate,
+ allow_new_users: params.allow_new_users,
})
.await?
};
diff --git a/tokenserver-db-common/src/params.rs b/tokenserver-db-common/src/params.rs
index 6151b0b056..64f808c85e 100644
--- a/tokenserver-db-common/src/params.rs
+++ b/tokenserver-db-common/src/params.rs
@@ -35,6 +35,7 @@ pub struct GetOrCreateUser {
pub client_state: String,
pub keys_changed_at: Option,
pub capacity_release_rate: Option,
+ pub allow_new_users: bool,
}
pub type AllocateUser = GetOrCreateUser;
diff --git a/tokenserver-db/src/tests.rs b/tokenserver-db/src/tests.rs
index 45cfd9b1cc..5fe62d2ca2 100644
--- a/tokenserver-db/src/tests.rs
+++ b/tokenserver-db/src/tests.rs
@@ -556,6 +556,7 @@ async fn test_node_allocation() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
assert_eq!(user.node, "https://node1");
@@ -609,6 +610,7 @@ async fn test_allocation_to_least_loaded_node() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -620,6 +622,7 @@ async fn test_allocation_to_least_loaded_node() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -663,6 +666,7 @@ async fn test_allocation_is_not_allowed_to_downed_nodes() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await;
let error = result.unwrap_err();
@@ -704,6 +708,7 @@ async fn test_allocation_is_not_allowed_to_backoff_nodes() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await;
let error = result.unwrap_err();
@@ -744,6 +749,7 @@ async fn test_node_reassignment_when_records_are_replaced() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
let user1 = db
@@ -768,6 +774,7 @@ async fn test_node_reassignment_when_records_are_replaced() -> DbResult<()> {
client_state: "bbbb".to_owned(),
keys_changed_at: Some(1235),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -816,6 +823,7 @@ async fn test_node_reassignment_not_done_for_retired_users() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -827,6 +835,7 @@ async fn test_node_reassignment_not_done_for_retired_users() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -884,6 +893,7 @@ async fn test_node_reassignment_and_removal() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -895,6 +905,7 @@ async fn test_node_reassignment_and_removal() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -906,6 +917,7 @@ async fn test_node_reassignment_and_removal() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -917,6 +929,7 @@ async fn test_node_reassignment_and_removal() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -949,6 +962,7 @@ async fn test_node_reassignment_and_removal() -> DbResult<()> {
client_state: user.client_state.clone(),
keys_changed_at: user.keys_changed_at,
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -978,6 +992,7 @@ async fn test_node_reassignment_and_removal() -> DbResult<()> {
client_state: user.client_state.clone(),
keys_changed_at: user.keys_changed_at,
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -1034,6 +1049,7 @@ async fn test_gradual_release_of_node_capacity() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -1051,6 +1067,7 @@ async fn test_gradual_release_of_node_capacity() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -1070,6 +1087,7 @@ async fn test_gradual_release_of_node_capacity() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -1087,6 +1105,7 @@ async fn test_gradual_release_of_node_capacity() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -1105,6 +1124,7 @@ async fn test_gradual_release_of_node_capacity() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -1122,6 +1142,7 @@ async fn test_gradual_release_of_node_capacity() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -1140,6 +1161,7 @@ async fn test_gradual_release_of_node_capacity() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await;
@@ -1185,6 +1207,7 @@ async fn test_correct_created_at_used_during_node_reassignment() -> DbResult<()>
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -1204,6 +1227,7 @@ async fn test_correct_created_at_used_during_node_reassignment() -> DbResult<()>
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -1245,6 +1269,7 @@ async fn test_correct_created_at_used_during_user_retrieval() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -1261,6 +1286,7 @@ async fn test_correct_created_at_used_during_user_retrieval() -> DbResult<()> {
client_state: "aaaa".to_owned(),
keys_changed_at: Some(1234),
capacity_release_rate: None,
+ allow_new_users: true,
})
.await?;
@@ -1270,6 +1296,46 @@ async fn test_correct_created_at_used_during_user_retrieval() -> DbResult<()> {
Ok(())
}
+#[tokio::test]
+async fn test_no_new_user_allocation() -> DbResult<()> {
+ let pool = db_pool().await?;
+ let mut db = pool.get().await?;
+
+ let service_id = db
+ .get_service_id(params::GetServiceId {
+ service: "sync-1.5".to_owned(),
+ })
+ .await?
+ .id;
+
+ // Add a node
+ db.post_node(params::PostNode {
+ service_id,
+ node: "https://node1".to_owned(),
+ current_load: 4,
+ capacity: 8,
+ available: 1,
+ ..Default::default()
+ })
+ .await?;
+
+ // Try to create a user
+ let result = db
+ .get_or_create_user(params::GetOrCreateUser {
+ service_id,
+ generation: 1234,
+ email: "test4@test.com".to_owned(),
+ client_state: "aaaa".to_owned(),
+ keys_changed_at: Some(1234),
+ capacity_release_rate: None,
+ allow_new_users: false,
+ })
+ .await;
+ assert!(result.unwrap_err().is_user_not_created());
+
+ Ok(())
+}
+
#[tokio::test]
async fn test_latest_created_at() -> DbResult<()> {
let pool = db_pool().await?;
diff --git a/tokenserver-settings/src/lib.rs b/tokenserver-settings/src/lib.rs
index c3d9fb8cd2..0c68d1d856 100644
--- a/tokenserver-settings/src/lib.rs
+++ b/tokenserver-settings/src/lib.rs
@@ -69,6 +69,9 @@ pub struct Settings {
/// Whether to enable the FxA webhook endpoint.
/// Defaults to false.
pub fxa_webhook_enabled: bool,
+ /// Whether new users may register to the Tokenserver.
+ /// Defaults to true.
+ pub allow_new_users: bool,
/// Whether the FxA webhook handler runs in metrics-only mode. When enabled, received events
/// are counted but not processed.
/// Defaults to false.
@@ -105,6 +108,7 @@ impl Default for Settings {
init_node_url: None,
init_node_capacity: 100000,
fxa_webhook_enabled: false,
+ allow_new_users: true,
fxa_webhook_metrics_only: false,
fxa_webhook_set_client_id: None,
fxa_webhook_set_issuer: None,