Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- added: `:authorizer` connection option to deny SQL actions on `DBConnection` managed connections via `Exqlite.Sqlite3.set_authorizer/2`.

## v0.40.0

- changed: Update sqlite to `3.53.4`.
Expand Down
15 changes: 14 additions & 1 deletion lib/exqlite/connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ defmodule Exqlite.Connection do
`1` disable the progress handler, which reduces per-op overhead but means
`interrupt/1` and `cancel/1` only take effect once SQLite returns from the
current call.
* `:authorizer` - A list of SQL actions to deny on the connection.
Example: `authorizer: [:attach, :detach]`. Defaults to `[]` (no authorizer).
See `Exqlite.Sqlite3.set_authorizer/2` for the list of action atoms.
* `:chunk_size` - The chunk size for bulk fetching. Defaults to `50`.
* `:key` - Optional key to set during database initialization. This PRAGMA
is often used to set up database level encryption.
Expand Down Expand Up @@ -554,6 +557,15 @@ defmodule Exqlite.Connection do
end
end

# Must run last in `do_connect/2`. Connection setup issues its own PRAGMA and
# SELECT statements, which would be rejected if the authorizer denied them.
defp set_authorizer(db, options) do
case Keyword.get(options, :authorizer, []) do
[] -> :ok
deny_list -> Sqlite3.set_authorizer(db, deny_list)
end
end

defp load_extensions(db, options) do
global_extensions = Application.get_env(:exqlite, :load_extensions, [])

Expand Down Expand Up @@ -601,7 +613,8 @@ defmodule Exqlite.Connection do
:ok <- set_soft_heap_limit(db, options),
:ok <- set_hard_heap_limit(db, options),
:ok <- load_extensions(db, options),
:ok <- deserialize(db, options) do
:ok <- deserialize(db, options),
:ok <- set_authorizer(db, options) do
state = %__MODULE__{
db: db,
default_transaction_mode:
Expand Down
47 changes: 47 additions & 0 deletions test/exqlite/connection_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,53 @@ defmodule Exqlite.ConnectionTest do

File.rm(path)
end

test "setting authorizer denies listed actions" do
path = Temp.path!()
other_path = Temp.path!()

{:ok, state} = Connection.connect(database: path, authorizer: [:attach])

assert {:error, "not authorized"} =
Sqlite3.execute(state.db, "ATTACH DATABASE '#{other_path}' AS other")

# Actions not in the deny list still work
assert :ok = Sqlite3.execute(state.db, "CREATE TABLE test (id INTEGER)")

File.rm(path)
File.rm(other_path)
end

test "authorizer is enforced through DBConnection callbacks" do
path = Temp.path!()
other_path = Temp.path!()

{:ok, state} = Connection.connect(database: path, authorizer: [:attach])
query = %Query{statement: "ATTACH DATABASE '#{other_path}' AS other"}

assert {:error, %Exqlite.Error{message: "not authorized"}, _state} =
Connection.handle_prepare(query, [], state)

File.rm(path)
File.rm(other_path)
end

test "authorizer does not interfere with connection setup" do
path = Temp.path!()

# Setup runs PRAGMA statements, so denying :pragma must not break connect
{:ok, state} =
Connection.connect(database: path, authorizer: [:pragma], journal_mode: :wal)

assert {:error, "not authorized"} =
Sqlite3.execute(state.db, "PRAGMA journal_mode = delete")

# Verify the setup pragma took effect via an unrestricted connection
{:ok, other} = Connection.connect(database: path)
assert {:ok, "wal"} = get_pragma(other.db, :journal_mode)

File.rm(path)
end
end

defp get_pragma(db, pragma_name) do
Expand Down
Loading