#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
extern crate libsqlite3_sys as ffi;
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use sqlite_wasm_rs as ffi;
use super::SqliteConnection;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateIndex<'a> {
pub index: Option<&'a str>,
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateTable<'a> {
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateTempIndex<'a> {
pub index: Option<&'a str>,
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateTempTable<'a> {
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateTempTrigger<'a> {
pub trigger: Option<&'a str>,
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateTempView<'a> {
pub view: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateTrigger<'a> {
pub trigger: Option<&'a str>,
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateView<'a> {
pub view: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Delete<'a> {
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DropIndex<'a> {
pub index: Option<&'a str>,
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DropTable<'a> {
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DropTempIndex<'a> {
pub index: Option<&'a str>,
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DropTempTable<'a> {
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DropTempTrigger<'a> {
pub trigger: Option<&'a str>,
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DropTempView<'a> {
pub view: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DropTrigger<'a> {
pub trigger: Option<&'a str>,
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DropView<'a> {
pub view: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Insert<'a> {
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Pragma<'a> {
pub name: Option<&'a str>,
pub argument: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Read<'a> {
pub table: Option<&'a str>,
pub column: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Select<'a> {
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Transaction<'a> {
pub operation: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Update<'a> {
pub table: Option<&'a str>,
pub column: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Attach<'a> {
pub filename: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Detach<'a> {
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct AlterTable<'a> {
pub database: Option<&'a str>,
pub table: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Reindex<'a> {
pub index: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Analyze<'a> {
pub table: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateVTable<'a> {
pub table: Option<&'a str>,
pub module: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DropVTable<'a> {
pub table: Option<&'a str>,
pub module: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Function<'a> {
pub function: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Savepoint<'a> {
pub operation: Option<&'a str>,
pub name: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Recursive<'a> {
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Unknown<'a> {
pub code: i32,
pub arg1: Option<&'a str>,
pub arg2: Option<&'a str>,
pub database: Option<&'a str>,
pub accessor: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AuthorizerContext<'a> {
CreateIndex(CreateIndex<'a>),
CreateTable(CreateTable<'a>),
CreateTempIndex(CreateTempIndex<'a>),
CreateTempTable(CreateTempTable<'a>),
CreateTempTrigger(CreateTempTrigger<'a>),
CreateTempView(CreateTempView<'a>),
CreateTrigger(CreateTrigger<'a>),
CreateView(CreateView<'a>),
Delete(Delete<'a>),
DropIndex(DropIndex<'a>),
DropTable(DropTable<'a>),
DropTempIndex(DropTempIndex<'a>),
DropTempTable(DropTempTable<'a>),
DropTempTrigger(DropTempTrigger<'a>),
DropTempView(DropTempView<'a>),
DropTrigger(DropTrigger<'a>),
DropView(DropView<'a>),
Insert(Insert<'a>),
Pragma(Pragma<'a>),
Read(Read<'a>),
Select(Select<'a>),
Transaction(Transaction<'a>),
Update(Update<'a>),
Attach(Attach<'a>),
Detach(Detach<'a>),
AlterTable(AlterTable<'a>),
Reindex(Reindex<'a>),
Analyze(Analyze<'a>),
CreateVTable(CreateVTable<'a>),
DropVTable(DropVTable<'a>),
Function(Function<'a>),
Savepoint(Savepoint<'a>),
Recursive(Recursive<'a>),
Unknown(Unknown<'a>),
}
impl<'a> AuthorizerContext<'a> {
pub fn is_schema_modifying(&self) -> bool {
matches!(
self,
Self::CreateIndex(_)
| Self::CreateTable(_)
| Self::CreateTempIndex(_)
| Self::CreateTempTable(_)
| Self::CreateTempTrigger(_)
| Self::CreateTempView(_)
| Self::CreateTrigger(_)
| Self::CreateView(_)
| Self::CreateVTable(_)
| Self::DropIndex(_)
| Self::DropTable(_)
| Self::DropTempIndex(_)
| Self::DropTempTable(_)
| Self::DropTempTrigger(_)
| Self::DropTempView(_)
| Self::DropTrigger(_)
| Self::DropView(_)
| Self::DropVTable(_)
| Self::AlterTable(_)
) || self.writes_schema_storage()
}
fn writes_schema_storage(&self) -> bool {
const SCHEMA_TABLES: [&str; 5] = [
"sqlite_schema",
"sqlite_master",
"sqlite_temp_schema",
"sqlite_temp_master",
"sqlite_dbpage",
];
match self {
Self::Insert(Insert { table, .. })
| Self::Delete(Delete { table, .. })
| Self::Update(Update { table, .. }) => table.is_some_and(|table| {
SCHEMA_TABLES
.iter()
.any(|schema_table| table.eq_ignore_ascii_case(schema_table))
}),
Self::Pragma(Pragma {
name: Some(name),
argument: Some(_),
..
}) => {
name.eq_ignore_ascii_case("writable_schema")
|| name.eq_ignore_ascii_case("schema_version")
}
_ => false,
}
}
pub(crate) fn from_ffi(
code: i32,
arg1: Option<&'a str>,
arg2: Option<&'a str>,
database: Option<&'a str>,
accessor: Option<&'a str>,
) -> Self {
match code {
ffi::SQLITE_CREATE_INDEX => Self::CreateIndex(CreateIndex {
index: arg1,
table: arg2,
database,
accessor,
}),
ffi::SQLITE_CREATE_TABLE => Self::CreateTable(CreateTable {
table: arg1,
database,
accessor,
}),
ffi::SQLITE_CREATE_TEMP_INDEX => Self::CreateTempIndex(CreateTempIndex {
index: arg1,
table: arg2,
database,
accessor,
}),
ffi::SQLITE_CREATE_TEMP_TABLE => Self::CreateTempTable(CreateTempTable {
table: arg1,
database,
accessor,
}),
ffi::SQLITE_CREATE_TEMP_TRIGGER => Self::CreateTempTrigger(CreateTempTrigger {
trigger: arg1,
table: arg2,
database,
accessor,
}),
ffi::SQLITE_CREATE_TEMP_VIEW => Self::CreateTempView(CreateTempView {
view: arg1,
database,
accessor,
}),
ffi::SQLITE_CREATE_TRIGGER => Self::CreateTrigger(CreateTrigger {
trigger: arg1,
table: arg2,
database,
accessor,
}),
ffi::SQLITE_CREATE_VIEW => Self::CreateView(CreateView {
view: arg1,
database,
accessor,
}),
ffi::SQLITE_DELETE => Self::Delete(Delete {
table: arg1,
database,
accessor,
}),
ffi::SQLITE_DROP_INDEX => Self::DropIndex(DropIndex {
index: arg1,
table: arg2,
database,
accessor,
}),
ffi::SQLITE_DROP_TABLE => Self::DropTable(DropTable {
table: arg1,
database,
accessor,
}),
ffi::SQLITE_DROP_TEMP_INDEX => Self::DropTempIndex(DropTempIndex {
index: arg1,
table: arg2,
database,
accessor,
}),
ffi::SQLITE_DROP_TEMP_TABLE => Self::DropTempTable(DropTempTable {
table: arg1,
database,
accessor,
}),
ffi::SQLITE_DROP_TEMP_TRIGGER => Self::DropTempTrigger(DropTempTrigger {
trigger: arg1,
table: arg2,
database,
accessor,
}),
ffi::SQLITE_DROP_TEMP_VIEW => Self::DropTempView(DropTempView {
view: arg1,
database,
accessor,
}),
ffi::SQLITE_DROP_TRIGGER => Self::DropTrigger(DropTrigger {
trigger: arg1,
table: arg2,
database,
accessor,
}),
ffi::SQLITE_DROP_VIEW => Self::DropView(DropView {
view: arg1,
database,
accessor,
}),
ffi::SQLITE_INSERT => Self::Insert(Insert {
table: arg1,
database,
accessor,
}),
ffi::SQLITE_PRAGMA => Self::Pragma(Pragma {
name: arg1,
argument: arg2,
database,
accessor,
}),
ffi::SQLITE_READ => Self::Read(Read {
table: arg1,
column: arg2,
database,
accessor,
}),
ffi::SQLITE_SELECT => Self::Select(Select { accessor }),
ffi::SQLITE_TRANSACTION => Self::Transaction(Transaction {
operation: arg1,
accessor,
}),
ffi::SQLITE_UPDATE => Self::Update(Update {
table: arg1,
column: arg2,
database,
accessor,
}),
ffi::SQLITE_ATTACH => Self::Attach(Attach {
filename: arg1,
accessor,
}),
ffi::SQLITE_DETACH => Self::Detach(Detach {
database: arg1,
accessor,
}),
ffi::SQLITE_ALTER_TABLE => Self::AlterTable(AlterTable {
database: arg1,
table: arg2,
accessor,
}),
ffi::SQLITE_REINDEX => Self::Reindex(Reindex {
index: arg1,
database,
accessor,
}),
ffi::SQLITE_ANALYZE => Self::Analyze(Analyze {
table: arg1,
database,
accessor,
}),
ffi::SQLITE_CREATE_VTABLE => Self::CreateVTable(CreateVTable {
table: arg1,
module: arg2,
database,
accessor,
}),
ffi::SQLITE_DROP_VTABLE => Self::DropVTable(DropVTable {
table: arg1,
module: arg2,
database,
accessor,
}),
ffi::SQLITE_FUNCTION => Self::Function(Function {
function: arg2,
accessor,
}),
ffi::SQLITE_SAVEPOINT => Self::Savepoint(Savepoint {
operation: arg1,
name: arg2,
accessor,
}),
ffi::SQLITE_RECURSIVE => Self::Recursive(Recursive { accessor }),
other => Self::Unknown(Unknown {
code: other,
arg1,
arg2,
database,
accessor,
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthorizerDecision {
Allow,
Ignore,
Deny,
}
impl AuthorizerDecision {
pub(crate) fn to_ffi(self) -> i32 {
match self {
Self::Allow => ffi::SQLITE_OK,
Self::Ignore => ffi::SQLITE_IGNORE,
Self::Deny => ffi::SQLITE_DENY,
}
}
}
impl SqliteConnection {
pub fn on_authorize<F>(&mut self, hook: F)
where
F: FnMut(AuthorizerContext<'_>) -> AuthorizerDecision + Send + 'static,
{
self.raw_connection.set_authorizer(hook);
self.statement_cache.clear();
}
pub fn remove_authorizer(&mut self) {
self.raw_connection.remove_authorizer();
self.statement_cache.clear();
}
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
use crate::connection::Connection;
use crate::query_dsl::RunQueryDsl;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
fn connection() -> SqliteConnection {
SqliteConnection::establish(":memory:").unwrap()
}
#[diesel_test_helper::test]
fn on_authorize_deny_rejects_statement() {
let conn = &mut connection();
crate::sql_query("CREATE TABLE auth_basic (id INTEGER PRIMARY KEY)")
.execute(conn)
.unwrap();
let calls = Arc::new(AtomicU32::new(0));
let calls2 = calls.clone();
conn.on_authorize(move |_ctx| {
calls2.fetch_add(1, Ordering::Relaxed);
AuthorizerDecision::Deny
});
let denied = crate::sql_query("SELECT id FROM auth_basic").execute(conn);
assert!(denied.is_err(), "a denied statement should fail to prepare");
assert!(
calls.load(Ordering::Relaxed) > 0,
"the authorizer callback should have been invoked"
);
conn.remove_authorizer();
crate::sql_query("SELECT id FROM auth_basic")
.execute(conn)
.unwrap();
}
#[diesel_test_helper::test]
fn remove_authorizer_re_prepares_cached_statements() {
use crate::prelude::*;
use crate::sqlite::AuthorizerContext;
crate::table! {
auth_ignore_items (id) {
id -> Integer,
}
}
let conn = &mut connection();
crate::sql_query("CREATE TABLE auth_ignore_items (id INTEGER PRIMARY KEY)")
.execute(conn)
.unwrap();
crate::sql_query("INSERT INTO auth_ignore_items (id) VALUES (42)")
.execute(conn)
.unwrap();
conn.on_authorize(|ctx| match ctx {
AuthorizerContext::Read(_) => AuthorizerDecision::Ignore,
_ => AuthorizerDecision::Allow,
});
let ignored = auth_ignore_items::table
.select(auth_ignore_items::id.nullable())
.load::<Option<i32>>(conn)
.unwrap();
assert_eq!(
ignored,
vec![None],
"Ignore substitutes NULL for the column"
);
conn.remove_authorizer();
let restored = auth_ignore_items::table
.select(auth_ignore_items::id.nullable())
.load::<Option<i32>>(conn)
.unwrap();
assert_eq!(
restored,
vec![Some(42)],
"after removing the authorizer the real value is returned"
);
}
#[diesel_test_helper::test]
fn on_authorize_re_prepares_cached_statements() {
use crate::prelude::*;
use crate::sqlite::AuthorizerContext;
crate::table! {
auth_replace_items (id) {
id -> Integer,
}
}
let conn = &mut connection();
crate::sql_query("CREATE TABLE auth_replace_items (id INTEGER PRIMARY KEY)")
.execute(conn)
.unwrap();
crate::sql_query("INSERT INTO auth_replace_items (id) VALUES (42)")
.execute(conn)
.unwrap();
conn.on_authorize(|_ctx| AuthorizerDecision::Allow);
let allowed = auth_replace_items::table
.select(auth_replace_items::id.nullable())
.load::<Option<i32>>(conn)
.unwrap();
assert_eq!(
allowed,
vec![Some(42)],
"the allow-all authorizer returns the real value"
);
conn.on_authorize(|ctx| match ctx {
AuthorizerContext::Read(_) => AuthorizerDecision::Ignore,
_ => AuthorizerDecision::Allow,
});
let ignored = auth_replace_items::table
.select(auth_replace_items::id.nullable())
.load::<Option<i32>>(conn)
.unwrap();
assert_eq!(
ignored,
vec![None],
"after replacing the authorizer the new decision takes effect"
);
}
#[diesel_test_helper::test]
#[expect(
unsafe_code,
reason = "a non-UTF-8 identifier cannot be written through `&str` SQL"
)]
fn non_utf8_names_are_denied_before_the_callback() {
use std::sync::Mutex;
let conn = &mut connection();
let sql = c"CREATE VIEW \"helper\xff\" AS SELECT abs(-1) AS id; CREATE VIEW items AS SELECT id FROM \"helper\xff\";";
let rc = unsafe {
conn.with_raw_connection(|raw| {
ffi::sqlite3_exec(
raw,
sql.as_ptr(),
None,
core::ptr::null_mut(),
core::ptr::null_mut(),
)
})
};
assert_eq!(rc, ffi::SQLITE_OK);
let abs_accessors = Arc::new(Mutex::new(Vec::new()));
let abs_accessors2 = abs_accessors.clone();
conn.on_authorize(move |ctx| {
if let AuthorizerContext::Function(function) = ctx
&& function.function == Some("abs")
{
abs_accessors2
.lock()
.unwrap()
.push(function.accessor.map(str::to_owned));
}
AuthorizerDecision::Allow
});
let result = crate::sql_query("SELECT id FROM items").execute(conn);
assert!(result.is_err());
assert!(abs_accessors.lock().unwrap().iter().all(Option::is_some));
}
#[diesel_test_helper::test]
fn schema_modifying_policy_blocks_writable_schema_edits() {
use crate::connection::SimpleConnection;
use crate::dsl::sql;
use crate::sql_types::BigInt;
let conn = &mut connection();
conn.batch_execute(
"CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER NOT NULL CHECK (balance >= 0));
CREATE TRIGGER audit AFTER UPDATE ON accounts BEGIN SELECT 1; END;",
)
.unwrap();
conn.on_authorize(|ctx| {
if ctx.is_schema_modifying() {
AuthorizerDecision::Deny
} else {
AuthorizerDecision::Allow
}
});
let _ = conn.batch_execute(
"PRAGMA writable_schema = ON;
DELETE FROM sqlite_schema WHERE name = 'audit';
PRAGMA writable_schema = RESET;",
);
assert!(conn.batch_execute("PRAGMA writable_schema").is_ok());
assert!(conn.batch_execute("PRAGMA schema_version").is_ok());
conn.remove_authorizer();
let triggers: i64 =
sql::<BigInt>("SELECT count(*) FROM sqlite_schema WHERE name = 'audit'")
.get_result(conn)
.unwrap();
assert_eq!(triggers, 1);
}
}