Limit some admin APIs in --demo mode to avoid most egregious jokes, e.g. changing the admin user or deleting system tables.

This commit is contained in:
Sebastian Jeltsch
2025-03-09 14:57:31 +01:00
parent 3dec360ba9
commit e1ac5354b6
11 changed files with 93 additions and 17 deletions
+7 -7
View File
@@ -6,13 +6,6 @@ server {
}
auth {
oauth_providers: [{
key: "discord"
value {
client_id: "invalid_discord_id"
client_secret: "<REDACTED>"
provider_id: DISCORD
}
}, {
key: "oidc0"
value {
client_id: "invalid_client_id"
@@ -23,6 +16,13 @@ auth {
token_url: "http://localhost:9088/token"
user_api_url: "http://localhost:9088/userinfo"
}
}, {
key: "discord"
value {
client_id: "invalid_discord_id"
client_secret: "<REDACTED>"
provider_id: DISCORD
}
}]
}
record_apis: [{
+14 -1
View File
@@ -41,7 +41,10 @@ pub async fn query_handler(
// Check the statements are correct before executing anything, just to be sure.
let statements =
sqlite3_parse_into_statements(&request.query).map_err(|err| Error::BadRequest(err.into()))?;
let mut must_invalidate_table_cache = false;
let mut mutation = false;
for stmt in statements {
use sqlite3_parser::ast::Stmt;
@@ -53,13 +56,23 @@ pub async fn query_handler(
| Stmt::CreateVirtualTable { .. }
| Stmt::CreateView { .. } => {
must_invalidate_table_cache = true;
mutation = true;
}
_ => {
Stmt::Select { .. } => {
// Do nothing.
}
_ => {
mutation = true;
}
}
}
if state.demo_mode() && mutation {
return Err(Error::Precondition(
"Demo disallows mutation queries".into(),
));
}
let batched_rows_result = state.conn().execute_batch(&request.query).await;
// In the fallback case we always need to invalidate the cache.
@@ -84,6 +84,10 @@ pub async fn delete_rows_handler(
Path(table_name): Path<String>,
Json(request): Json<DeleteRowsRequest>,
) -> Result<Response, Error> {
if state.demo_mode() && table_name.starts_with("_") {
return Err(Error::Precondition("Disallowed in demo".into()));
}
let DeleteRowsRequest {
primary_key_column,
values,
@@ -29,6 +29,10 @@ pub async fn update_row_handler(
Path(table_name): Path<String>,
Json(request): Json<UpdateRowRequest>,
) -> Result<(), Error> {
if state.demo_mode() && table_name.starts_with("_") {
return Err(Error::Precondition("Disallowed in demo".into()));
}
let Some(table_metadata) = state.table_metadata().get(&table_name) else {
return Err(Error::Precondition(format!("Table {table_name} not found")));
};
@@ -27,6 +27,10 @@ pub async fn alter_index_handler(
State(state): State<AppState>,
Json(request): Json<AlterIndexRequest>,
) -> Result<Response, Error> {
if state.demo_mode() && request.source_schema.name.starts_with("_") {
return Err(Error::Precondition("Disallowed in demo".into()));
}
let source_schema = request.source_schema;
let source_index_name = source_schema.name.clone();
let target_schema = request.target_schema;
@@ -29,6 +29,10 @@ pub async fn alter_table_handler(
State(state): State<AppState>,
Json(request): Json<AlterTableRequest>,
) -> Result<Response, Error> {
if state.demo_mode() && request.source_schema.name.starts_with("_") {
return Err(Error::Precondition("Disallowed in demo".into()));
}
let source_schema = request.source_schema;
let source_table_name = source_schema.name.clone();
@@ -23,6 +23,9 @@ pub async fn drop_index_handler(
Json(request): Json<DropIndexRequest>,
) -> Result<Response, Error> {
let index_name = request.name;
if state.demo_mode() && index_name.starts_with("_") {
return Err(Error::Precondition("Disallowed in demo".into()));
}
let migration_path = state.data_dir().migrations_path();
let conn = state.conn();
+6 -3
View File
@@ -23,8 +23,10 @@ pub async fn drop_table_handler(
State(state): State<AppState>,
Json(request): Json<DropTableRequest>,
) -> Result<Response, Error> {
let conn = state.conn();
let table_name = &request.name;
if state.demo_mode() && table_name.starts_with("_") {
return Err(Error::Precondition("Disallowed in demo".into()));
}
let entity_type: &str;
if state.table_metadata().get(table_name).is_some() {
@@ -40,7 +42,8 @@ pub async fn drop_table_handler(
let writer = {
let table_name = table_name.clone();
let migration_path = state.data_dir().migrations_path();
conn
state
.conn()
.call(move |conn| {
let mut tx = TransactionRecorder::new(
conn,
@@ -75,7 +78,7 @@ pub async fn drop_table_handler(
// Write migration file and apply it right away.
if let Some(writer) = writer {
let _report = writer.write(conn).await?;
let _report = writer.write(state.conn()).await?;
}
state.table_metadata().invalidate_all().await?;
+14 -3
View File
@@ -8,21 +8,32 @@ use serde::Deserialize;
use ts_rs::TS;
use crate::admin::rows::delete_row;
use crate::admin::user::is_demo_admin;
use crate::admin::AdminError as Error;
use crate::app_state::AppState;
use crate::util::uuid_to_b64;
#[derive(Debug, Deserialize, Default, TS)]
#[ts(export)]
pub struct DeleteUserRequest {
#[ts(type = "string")]
id: serde_json::Value,
id: uuid::Uuid,
}
pub async fn delete_user_handler(
State(state): State<AppState>,
Json(request): Json<DeleteUserRequest>,
) -> Result<Response, Error> {
delete_row(&state, "_user", "id", request.id).await?;
if state.demo_mode() && is_demo_admin(&state, &request.id).await {
return Err(Error::Precondition("Deleting demo admin forbidden".into()));
}
delete_row(
&state,
"_user",
"id",
serde_json::Value::String(uuid_to_b64(&request.id)),
)
.await?;
return Ok((StatusCode::OK, "deleted").into_response());
}
+26
View File
@@ -1,3 +1,9 @@
use trailbase_sqlite::params;
use uuid::Uuid;
use crate::constants::USER_TABLE;
use crate::AppState;
mod create_user;
mod delete_user;
mod list_users;
@@ -8,6 +14,26 @@ pub(super) use delete_user::delete_user_handler;
pub(super) use list_users::list_users_handler;
pub(super) use update_user::update_user_handler;
pub async fn is_demo_admin(state: &AppState, id: &Uuid) -> bool {
assert!(state.demo_mode());
let userid: [u8; 16] = id.into_bytes();
return match state
.user_conn()
.query_value(
&format!("SELECT EXISTS(SELECT * FROM {USER_TABLE} WHERE id=$1 AND email='admin@localhost')"),
params!(userid),
)
.await
{
Ok(value) => value.unwrap_or(true),
Err(err) => {
log::error!("{err}");
true
}
};
}
#[cfg(test)]
pub(crate) use create_user::create_user_for_test;
+7 -3
View File
@@ -9,6 +9,7 @@ use rusqlite::params;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::admin::user::is_demo_admin;
use crate::admin::AdminError as Error;
use crate::app_state::AppState;
use crate::auth::password::hash_password;
@@ -28,8 +29,9 @@ pub async fn update_user_handler(
State(state): State<AppState>,
Json(request): Json<UpdateUserRequest>,
) -> Result<Response, Error> {
let conn = state.user_conn();
let user_id_bytes = request.id.into_bytes();
if state.demo_mode() && is_demo_admin(&state, &request.id).await {
return Err(Error::Precondition("Updating demo admin forbidden".into()));
}
let hashed_password = match &request.password {
Some(pw) => Some(hash_password(pw)?),
@@ -50,10 +52,12 @@ pub async fn update_user_handler(
let email = request.email.clone();
let verified = request.verified;
conn
state
.user_conn()
.call(move |conn| {
let tx = conn.transaction()?;
let user_id_bytes: [u8; 16] = request.id.into_bytes();
if let Some(email) = email {
tx.execute(&UPDATE_EMAIL_QUERY, params![email, user_id_bytes])?;
}