ZetCode

Python sqlite3.Connection.set_authorizer Method

Last modified April 15, 2025

This comprehensive guide explores Python's sqlite3.Connection.set_authorizer method, which provides fine-grained control over database operations. We'll cover its purpose, callback mechanics, and practical security applications.

Basic Definitions

The set_authorizer method registers an authorization callback that SQLite invokes when attempting to perform database operations. This enables implementing custom security policies.

Key characteristics: it intercepts all SQL operations, allows or denies actions, and provides detailed context about each operation. The callback returns sqlite3.SQLITE_OK to allow or other codes to deny.

Basic Authorization Example

This example shows a simple authorizer that logs all database operations without restricting any actions.

basic_authorizer.py
import sqlite3

def authorizer_callback(action, arg1, arg2, dbname, source):
    print(f"Action: {action}, Table: {arg1}, Column: {arg2}")
    return sqlite3.SQLITE_OK

with sqlite3.connect(":memory:") as conn:
    conn.set_authorizer(authorizer_callback)
    cursor = conn.cursor()
    
    cursor.execute("CREATE TABLE users (id INTEGER, name TEXT)")
    cursor.execute("INSERT INTO users VALUES (1, 'Alice')")
    cursor.execute("SELECT * FROM users")
    
    print(cursor.fetchall())

The authorizer callback receives details about each operation. In this case, it simply logs the actions while allowing everything by returning SQLITE_OK.

This pattern is useful for auditing database activity or debugging complex applications with many SQL operations.

Read-Only Database Protection

This example implements a read-only database by denying all write operations through the authorizer callback.

readonly_protection.py
import sqlite3

def read_only_authorizer(action, arg1, arg2, dbname, source):
    # Allow SELECT operations
    if action == sqlite3.SQLITE_SELECT:
        return sqlite3.SQLITE_OK
    # Deny all other operations
    return sqlite3.SQLITE_DENY

with sqlite3.connect(":memory:") as conn:
    conn.set_authorizer(read_only_authorizer)
    cursor = conn.cursor()
    
    # This will work
    cursor.execute("SELECT 1")
    print(cursor.fetchone())
    
    # This will fail
    try:
        cursor.execute("CREATE TABLE test (id INTEGER)")
    except sqlite3.DatabaseError as e:
        print(f"Operation denied: {e}")

The callback checks the action type and only allows SELECT statements. All other operations are denied by returning SQLITE_DENY, which raises a DatabaseError.

This approach is useful when you need to ensure data integrity by preventing modifications, such as in reporting applications.

Table-Specific Permissions

This example demonstrates implementing table-level permissions by restricting access to specific tables based on their names.

table_permissions.py
import sqlite3

def table_authorizer(action, arg1, arg2, dbname, source):
    # Block all access to 'sensitive_data' table
    if arg1 == "sensitive_data":
        return sqlite3.SQLITE_DENY
    # Allow everything else
    return sqlite3.SQLITE_OK

with sqlite3.connect(":memory:") as conn:
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE users (id INTEGER, name TEXT)")
    cursor.execute("CREATE TABLE sensitive_data (ssn TEXT, salary REAL)")
    
    conn.set_authorizer(table_authorizer)
    
    # This will work
    cursor.execute("SELECT * FROM users")
    
    # This will fail
    try:
        cursor.execute("SELECT * FROM sensitive_data")
    except sqlite3.DatabaseError as e:
        print(f"Access denied: {e}")

The authorizer checks the table name (arg1) and blocks all access to the 'sensitive_data' table while allowing operations on other tables.

This pattern is valuable for implementing row-level security or multi-tenant applications where different users should have access to different data subsets.

Column-Level Protection

This example shows how to protect specific columns in a table while allowing access to others, implementing column-level security.

column_protection.py
import sqlite3

def column_authorizer(action, arg1, arg2, dbname, source):
    # Block access to 'password' column in any table
    if arg2 == "password":
        return sqlite3.SQLITE_DENY
    # Allow everything else
    return sqlite3.SQLITE_OK

with sqlite3.connect(":memory:") as conn:
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE accounts (id INTEGER, username TEXT, password TEXT)")
    cursor.execute("INSERT INTO accounts VALUES (1, 'admin', 'secret')")
    
    conn.set_authorizer(column_authorizer)
    
    # This will work
    cursor.execute("SELECT id, username FROM accounts")
    print(cursor.fetchall())
    
    # This will fail
    try:
        cursor.execute("SELECT password FROM accounts")
    except sqlite3.DatabaseError as e:
        print(f"Access denied: {e}")

The callback examines the column name (arg2) and blocks any access to columns named 'password', while allowing access to other columns.

This technique is essential for protecting sensitive fields like passwords, API keys, or personal identification information in database applications.

Operation-Specific Authorization

This example implements different rules for different types of SQL operations, allowing more granular control over database access.

operation_authorization.py
import sqlite3

def operation_authorizer(action, arg1, arg2, dbname, source):
    # Allow all read operations
    if action in (sqlite3.SQLITE_SELECT, sqlite3.SQLITE_READ):
        return sqlite3.SQLITE_OK
    # Allow table creation but not modification
    if action == sqlite3.SQLITE_CREATE_TABLE:
        return sqlite3.SQLITE_OK
    # Deny all other operations
    return sqlite3.SQLITE_DENY

with sqlite3.connect(":memory:") as conn:
    conn.set_authorizer(operation_authorizer)
    cursor = conn.cursor()
    
    # This will work (table creation)
    cursor.execute("CREATE TABLE logs (id INTEGER, message TEXT)")
    
    # This will work (reading)
    cursor.execute("SELECT name FROM sqlite_master")
    print(cursor.fetchall())
    
    # This will fail (insert)
    try:
        cursor.execute("INSERT INTO logs VALUES (1, 'test')")
    except sqlite3.DatabaseError as e:
        print(f"Operation denied: {e}")

The authorizer distinguishes between different operation types using the action parameter. It allows read operations and table creation but blocks modifications.

This approach is useful for implementing complex security policies where different types of operations require different permission levels.

Dynamic Permission Checking

This advanced example shows how to implement dynamic permissions that can change during runtime based on application state.

dynamic_permissions.py
import sqlite3

class DynamicAuthorizer:
    def __init__(self):
        self.read_only_mode = True
    
    def authorize(self, action, arg1, arg2, dbname, source):
        if self.read_only_mode and action != sqlite3.SQLITE_SELECT:
            return sqlite3.SQLITE_DENY
        return sqlite3.SQLITE_OK

with sqlite3.connect(":memory:") as conn:
    authorizer = DynamicAuthorizer()
    conn.set_authorizer(authorizer.authorize)
    cursor = conn.cursor()
    
    # Initial state: read-only
    try:
        cursor.execute("CREATE TABLE test (id INTEGER)")
    except sqlite3.DatabaseError:
        print("Create denied in read-only mode")
    
    # Change to write mode
    authorizer.read_only_mode = False
    cursor.execute("CREATE TABLE test (id INTEGER)")
    print("Table created successfully")
    
    # Verify
    cursor.execute("SELECT name FROM sqlite_master")
    print(cursor.fetchall())

The authorizer maintains internal state (read_only_mode) that affects its decisions. This allows changing security policies dynamically during runtime.

This pattern is powerful for applications needing different access levels at different times, such as during maintenance vs normal operation.

Combining Multiple Authorization Rules

This final example demonstrates combining multiple authorization checks into a single comprehensive security policy.

combined_rules.py
import sqlite3

def comprehensive_authorizer(action, arg1, arg2, dbname, source):
    # Block all DROP operations
    if action == sqlite3.SQLITE_DROP_TABLE:
        return sqlite3.SQLITE_DENY
    
    # Block access to system tables (sqlite_ prefix)
    if arg1 and arg1.startswith("sqlite_"):
        return sqlite3.SQLITE_DENY
    
    # Block schema modifications except by direct connection
    if action in (sqlite3.SQLITE_ALTER_TABLE, sqlite3.SQLITE_CREATE_INDEX):
        if source != "main":
            return sqlite3.SQLITE_DENY
    
    # Allow everything else
    return sqlite3.SQLITE_OK

with sqlite3.connect(":memory:") as conn:
    conn.set_authorizer(comprehensive_authorizer)
    cursor = conn.cursor()
    
    # Allowed operations
    cursor.execute("CREATE TABLE users (id INTEGER)")
    cursor.execute("INSERT INTO users VALUES (1)")
    
    # Blocked operations
    try:
        cursor.execute("DROP TABLE users")
    except sqlite3.DatabaseError as e:
        print(f"Operation denied: {e}")
    
    try:
        cursor.execute("SELECT * FROM sqlite_master")
    except sqlite3.DatabaseError as e:
        print(f"Access denied: {e}")

The authorizer implements multiple security rules: preventing table drops, blocking access to SQLite system tables, and restricting schema modifications.

This comprehensive approach is typical of production applications where multiple security concerns must be addressed simultaneously.

Best Practices

Source References

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.