Skip to content

Commit 01fbd31

Browse files
adds set_authorizer/2 (fixes #344)
1 parent 552eb8b commit 01fbd31

4 files changed

Lines changed: 263 additions & 0 deletions

File tree

c_src/sqlite3_nif.c

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,17 @@ static sqlite3_mem_methods default_alloc_methods = {0};
4848
ErlNifPid* log_hook_pid = NULL;
4949
ErlNifMutex* log_hook_mutex = NULL;
5050

51+
// Denied authorizer action codes. Sized to 64 for margin — highest
52+
// currently defined SQLite action code is SQLITE_RECURSIVE (33).
53+
#define AUTHORIZER_DENY_SIZE 64
54+
5155
typedef struct connection
5256
{
5357
sqlite3* db;
5458
ErlNifMutex* mutex;
5559
ErlNifMutex* interrupt_mutex;
5660
ErlNifPid update_hook_pid;
61+
int authorizer_deny[AUTHORIZER_DENY_SIZE];
5762
} connection_t;
5863

5964
typedef struct statement
@@ -340,6 +345,7 @@ exqlite_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
340345
conn->db = db;
341346
conn->mutex = mutex;
342347
conn->interrupt_mutex = enif_mutex_create("exqlite:interrupt");
348+
memset(conn->authorizer_deny, 0, sizeof(conn->authorizer_deny));
343349
if (conn->interrupt_mutex == NULL) {
344350
// conn->db and conn->mutex are set; the destructor will clean them up.
345351
enif_release_resource(conn);
@@ -1425,6 +1431,128 @@ exqlite_set_update_hook(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
14251431
return am_ok;
14261432
}
14271433

1434+
//
1435+
// Authorizer
1436+
//
1437+
1438+
static int
1439+
authorizer_callback(void* user_data, int action, const char* arg1,
1440+
const char* arg2, const char* db_name, const char* trigger)
1441+
{
1442+
connection_t* conn = (connection_t*)user_data;
1443+
if (action >= 0 && action < AUTHORIZER_DENY_SIZE && conn->authorizer_deny[action]) {
1444+
return SQLITE_DENY;
1445+
}
1446+
return SQLITE_OK;
1447+
}
1448+
1449+
// Maps atom names to SQLite authorizer action codes
1450+
static int
1451+
action_code_from_atom(ErlNifEnv* env, ERL_NIF_TERM atom)
1452+
{
1453+
char buf[32];
1454+
if (!enif_get_atom(env, atom, buf, sizeof(buf), ERL_NIF_LATIN1)) {
1455+
return -1;
1456+
}
1457+
1458+
if (strcmp(buf, "create_index") == 0) return SQLITE_CREATE_INDEX;
1459+
if (strcmp(buf, "create_table") == 0) return SQLITE_CREATE_TABLE;
1460+
if (strcmp(buf, "create_temp_index") == 0) return SQLITE_CREATE_TEMP_INDEX;
1461+
if (strcmp(buf, "create_temp_table") == 0) return SQLITE_CREATE_TEMP_TABLE;
1462+
if (strcmp(buf, "create_temp_trigger") == 0) return SQLITE_CREATE_TEMP_TRIGGER;
1463+
if (strcmp(buf, "create_temp_view") == 0) return SQLITE_CREATE_TEMP_VIEW;
1464+
if (strcmp(buf, "create_trigger") == 0) return SQLITE_CREATE_TRIGGER;
1465+
if (strcmp(buf, "create_view") == 0) return SQLITE_CREATE_VIEW;
1466+
if (strcmp(buf, "delete") == 0) return SQLITE_DELETE;
1467+
if (strcmp(buf, "drop_index") == 0) return SQLITE_DROP_INDEX;
1468+
if (strcmp(buf, "drop_table") == 0) return SQLITE_DROP_TABLE;
1469+
if (strcmp(buf, "drop_temp_index") == 0) return SQLITE_DROP_TEMP_INDEX;
1470+
if (strcmp(buf, "drop_temp_table") == 0) return SQLITE_DROP_TEMP_TABLE;
1471+
if (strcmp(buf, "drop_temp_trigger") == 0) return SQLITE_DROP_TEMP_TRIGGER;
1472+
if (strcmp(buf, "drop_temp_view") == 0) return SQLITE_DROP_TEMP_VIEW;
1473+
if (strcmp(buf, "drop_trigger") == 0) return SQLITE_DROP_TRIGGER;
1474+
if (strcmp(buf, "drop_view") == 0) return SQLITE_DROP_VIEW;
1475+
if (strcmp(buf, "insert") == 0) return SQLITE_INSERT;
1476+
if (strcmp(buf, "pragma") == 0) return SQLITE_PRAGMA;
1477+
if (strcmp(buf, "read") == 0) return SQLITE_READ;
1478+
if (strcmp(buf, "select") == 0) return SQLITE_SELECT;
1479+
if (strcmp(buf, "transaction") == 0) return SQLITE_TRANSACTION;
1480+
if (strcmp(buf, "update") == 0) return SQLITE_UPDATE;
1481+
if (strcmp(buf, "attach") == 0) return SQLITE_ATTACH;
1482+
if (strcmp(buf, "detach") == 0) return SQLITE_DETACH;
1483+
if (strcmp(buf, "alter_table") == 0) return SQLITE_ALTER_TABLE;
1484+
if (strcmp(buf, "reindex") == 0) return SQLITE_REINDEX;
1485+
if (strcmp(buf, "analyze") == 0) return SQLITE_ANALYZE;
1486+
if (strcmp(buf, "create_vtable") == 0) return SQLITE_CREATE_VTABLE;
1487+
if (strcmp(buf, "drop_vtable") == 0) return SQLITE_DROP_VTABLE;
1488+
if (strcmp(buf, "function") == 0) return SQLITE_FUNCTION;
1489+
if (strcmp(buf, "savepoint") == 0) return SQLITE_SAVEPOINT;
1490+
if (strcmp(buf, "recursive") == 0) return SQLITE_RECURSIVE;
1491+
1492+
return -1;
1493+
}
1494+
1495+
// set_authorizer(conn, deny_list) -> :ok | {:error, reason}
1496+
// deny_list is a list of atoms: [:attach, :detach, :pragma, ...]
1497+
// Pass an empty list to clear the authorizer.
1498+
ERL_NIF_TERM
1499+
exqlite_set_authorizer(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
1500+
{
1501+
assert(env);
1502+
connection_t* conn = NULL;
1503+
1504+
if (argc != 2) {
1505+
return enif_make_badarg(env);
1506+
}
1507+
1508+
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
1509+
return am_invalid_connection;
1510+
}
1511+
1512+
connection_acquire_lock(conn);
1513+
1514+
if (conn->db == NULL) {
1515+
connection_release_lock(conn);
1516+
return make_error_tuple(env, am_connection_closed);
1517+
}
1518+
1519+
// Parse the deny list
1520+
unsigned int list_len;
1521+
if (!enif_get_list_length(env, argv[1], &list_len)) {
1522+
connection_release_lock(conn);
1523+
return enif_make_badarg(env);
1524+
}
1525+
1526+
if (list_len == 0) {
1527+
// Empty list: clear the authorizer
1528+
memset(conn->authorizer_deny, 0, sizeof(conn->authorizer_deny));
1529+
sqlite3_set_authorizer(conn->db, NULL, NULL);
1530+
connection_release_lock(conn);
1531+
return am_ok;
1532+
}
1533+
1534+
// Validate all atoms before mutating state — a bad atom in the list
1535+
// should not clear an existing authorizer as a side effect.
1536+
int new_deny[AUTHORIZER_DENY_SIZE] = {0};
1537+
ERL_NIF_TERM head, tail = argv[1];
1538+
while (enif_get_list_cell(env, tail, &head, &tail)) {
1539+
int code = action_code_from_atom(env, head);
1540+
if (code < 0 || code >= AUTHORIZER_DENY_SIZE) {
1541+
connection_release_lock(conn);
1542+
return enif_make_badarg(env);
1543+
}
1544+
new_deny[code] = 1;
1545+
}
1546+
1547+
// Validation passed — apply atomically
1548+
memcpy(conn->authorizer_deny, new_deny, sizeof(conn->authorizer_deny));
1549+
sqlite3_set_authorizer(conn->db, authorizer_callback, conn);
1550+
1551+
connection_release_lock(conn);
1552+
1553+
return am_ok;
1554+
}
1555+
14281556
//
14291557
// Log Notifications
14301558
//
@@ -1590,6 +1718,7 @@ static ErlNifFunc nif_funcs[] = {
15901718
{"release", 2, exqlite_release, ERL_NIF_DIRTY_JOB_IO_BOUND},
15911719
{"enable_load_extension", 2, exqlite_enable_load_extension, ERL_NIF_DIRTY_JOB_IO_BOUND},
15921720
{"set_update_hook", 2, exqlite_set_update_hook, ERL_NIF_DIRTY_JOB_IO_BOUND},
1721+
{"set_authorizer", 2, exqlite_set_authorizer, ERL_NIF_DIRTY_JOB_IO_BOUND},
15931722
{"set_log_hook", 1, exqlite_set_log_hook, ERL_NIF_DIRTY_JOB_IO_BOUND},
15941723
{"interrupt", 1, exqlite_interrupt, ERL_NIF_DIRTY_JOB_IO_BOUND},
15951724
{"errmsg", 1, exqlite_errmsg},

lib/exqlite/sqlite3.ex

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,38 @@ defmodule Exqlite.Sqlite3 do
405405
Sqlite3NIF.set_update_hook(conn, pid)
406406
end
407407

408+
@doc """
409+
Set an authorizer that denies specific SQL operations.
410+
411+
Accepts a list of action atoms to deny. Any SQL statement that triggers a
412+
denied action will fail with a "not authorized" error during preparation.
413+
414+
Pass an empty list to clear the authorizer.
415+
416+
## Action atoms
417+
418+
`:attach`, `:detach`, `:pragma`, `:insert`, `:update`, `:delete`,
419+
`:create_table`, `:drop_table`, `:create_index`, `:drop_index`,
420+
`:create_trigger`, `:drop_trigger`, `:create_view`, `:drop_view`,
421+
`:alter_table`, `:reindex`, `:analyze`, `:function`, `:savepoint`,
422+
`:transaction`, `:read`, `:select`, `:recursive`,
423+
`:create_temp_table`, `:create_temp_index`, `:create_temp_trigger`,
424+
`:create_temp_view`, `:drop_temp_table`, `:drop_temp_index`,
425+
`:drop_temp_trigger`, `:drop_temp_view`, `:create_vtable`, `:drop_vtable`
426+
427+
## Examples
428+
429+
# Block ATTACH and DETACH (prevent cross-database reads)
430+
:ok = Sqlite3.set_authorizer(conn, [:attach, :detach])
431+
432+
# Clear the authorizer
433+
:ok = Sqlite3.set_authorizer(conn, [])
434+
"""
435+
@spec set_authorizer(db(), [atom()]) :: :ok | {:error, reason()}
436+
def set_authorizer(conn, deny_list) when is_list(deny_list) do
437+
Sqlite3NIF.set_authorizer(conn, deny_list)
438+
end
439+
408440
@doc """
409441
Send log messages to a process.
410442

lib/exqlite/sqlite3_nif.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ defmodule Exqlite.Sqlite3NIF do
6666
@spec set_update_hook(db(), pid()) :: :ok | {:error, reason()}
6767
def set_update_hook(_conn, _pid), do: :erlang.nif_error(:not_loaded)
6868

69+
@spec set_authorizer(db(), [atom()]) :: :ok | {:error, reason()}
70+
def set_authorizer(_conn, _deny_list), do: :erlang.nif_error(:not_loaded)
71+
6972
@spec set_log_hook(pid()) :: :ok | {:error, reason()}
7073
def set_log_hook(_pid), do: :erlang.nif_error(:not_loaded)
7174

test/exqlite/sqlite3_test.exs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,105 @@ defmodule Exqlite.Sqlite3Test do
762762
end
763763
end
764764

765+
describe "set_authorizer/2" do
766+
setup do
767+
{:ok, path} = Temp.path()
768+
{:ok, conn} = Sqlite3.open(path)
769+
:ok = Sqlite3.execute(conn, "create table test(num integer)")
770+
771+
{:ok, other_path} = Temp.path()
772+
{:ok, other_conn} = Sqlite3.open(other_path)
773+
:ok = Sqlite3.execute(other_conn, "create table other(val text)")
774+
:ok = Sqlite3.execute(other_conn, "insert into other values ('secret')")
775+
Sqlite3.close(other_conn)
776+
777+
on_exit(fn ->
778+
Sqlite3.close(conn)
779+
File.rm(path)
780+
File.rm(other_path)
781+
end)
782+
783+
[conn: conn, other_path: other_path]
784+
end
785+
786+
test "denies ATTACH when :attach is in deny list", context do
787+
:ok = Sqlite3.set_authorizer(context.conn, [:attach])
788+
789+
assert {:error, "not authorized"} =
790+
Sqlite3.execute(
791+
context.conn,
792+
"ATTACH DATABASE '#{context.other_path}' AS other_db"
793+
)
794+
end
795+
796+
test "allows normal operations when only :attach is denied", context do
797+
:ok = Sqlite3.set_authorizer(context.conn, [:attach])
798+
:ok = Sqlite3.execute(context.conn, "insert into test values (42)")
799+
800+
{:ok, stmt} = Sqlite3.prepare(context.conn, "select * from test")
801+
assert {:row, [42]} = Sqlite3.step(context.conn, stmt)
802+
Sqlite3.release(context.conn, stmt)
803+
end
804+
805+
test "clears authorizer with empty deny list", context do
806+
:ok = Sqlite3.set_authorizer(context.conn, [:attach])
807+
808+
assert {:error, "not authorized"} =
809+
Sqlite3.execute(
810+
context.conn,
811+
"ATTACH DATABASE '#{context.other_path}' AS other_db"
812+
)
813+
814+
:ok = Sqlite3.set_authorizer(context.conn, [])
815+
816+
assert :ok =
817+
Sqlite3.execute(
818+
context.conn,
819+
"ATTACH DATABASE '#{context.other_path}' AS other_db2"
820+
)
821+
end
822+
823+
test "denies multiple action types", context do
824+
:ok = Sqlite3.set_authorizer(context.conn, [:attach, :detach, :pragma])
825+
826+
assert {:error, "not authorized"} =
827+
Sqlite3.execute(
828+
context.conn,
829+
"ATTACH DATABASE '#{context.other_path}' AS other_db"
830+
)
831+
832+
assert {:error, "not authorized"} =
833+
Sqlite3.execute(context.conn, "PRAGMA table_info(test)")
834+
end
835+
836+
test "denies savepoint (high action code)", context do
837+
:ok = Sqlite3.set_authorizer(context.conn, [:savepoint])
838+
assert {:error, "not authorized"} = Sqlite3.execute(context.conn, "SAVEPOINT sp1")
839+
end
840+
841+
test "does not clear existing authorizer on invalid input", context do
842+
:ok = Sqlite3.set_authorizer(context.conn, [:attach])
843+
844+
# Bad atom should raise without clearing the existing :attach deny
845+
assert_raise ArgumentError, fn ->
846+
Sqlite3.set_authorizer(context.conn, [:attach, :not_a_real_action])
847+
end
848+
849+
# Original authorizer should still be active
850+
assert {:error, "not authorized"} =
851+
Sqlite3.execute(
852+
context.conn,
853+
"ATTACH DATABASE '#{context.other_path}' AS other_db"
854+
)
855+
end
856+
857+
test "raises for invalid action atoms", context do
858+
assert_raise ArgumentError, fn ->
859+
Sqlite3.set_authorizer(context.conn, [:not_a_real_action])
860+
end
861+
end
862+
end
863+
765864
describe "set_log_hook/1" do
766865
setup do
767866
{:ok, conn} = Sqlite3.open(":memory:")

0 commit comments

Comments
 (0)