mirror of
https://github.com/trailbaseio/trailbase.git
synced 2026-05-23 02:28:34 -05:00
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:
@@ -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: [{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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])?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user