// Copyright 2021 Ross Light // SPDX-License-Identifier: ISC package sqlite import ( "fmt" "strings" "sync" "unsafe" "modernc.org/libc" lib "modernc.org/sqlite/lib" ) // An Authorizer is called during statement preparation to see whether an action // is allowed by the application. An Authorizer must not modify the database // connection, including by preparing statements. // // See https://sqlite.org/c3ref/set_authorizer.html for a longer explanation. type Authorizer interface { Authorize(Action) AuthResult } // SetAuthorizer registers an authorizer for the database connection. // SetAuthorizer(nil) clears any authorizer previously set. func (c *Conn) SetAuthorizer(auth Authorizer) error { if c == nil { return fmt.Errorf("sqlite: set authorizer: nil connection") } if auth == nil { c.releaseAuthorizer() res := ResultCode(lib.Xsqlite3_set_authorizer(c.tls, c.conn, 0, 0)) if err := reserr(res); err != nil { return fmt.Errorf("sqlite: set authorizer: %w", err) } return nil } authorizers.mu.Lock() if authorizers.m == nil { authorizers.m = make(map[uintptr]Authorizer) } authorizers.m[c.conn] = auth authorizers.mu.Unlock() // The following is a conversion from function value to uintptr. It assumes // the memory representation described in https://golang.org/s/go11func. // // It does this by doing the following in order: // 1) Create a Go struct containing a pointer to a pointer to // authTrampoline. It is assumed that the pointer to authTrampoline will be // stored in the read-only data section and thus will not move. // 2) Convert the pointer to the Go struct to a pointer to uintptr through // unsafe.Pointer. This is permitted via Rule #1 of unsafe.Pointer. // 3) Dereference the pointer to uintptr to obtain the function value as a // uintptr. This is safe as long as function values are passed as pointers. xAuth := *(*uintptr)(unsafe.Pointer(&struct { f func(*libc.TLS, uintptr, int32, uintptr, uintptr, uintptr, uintptr) int32 }{authTrampoline})) res := ResultCode(lib.Xsqlite3_set_authorizer(c.tls, c.conn, xAuth, c.conn)) if err := reserr(res); err != nil { return fmt.Errorf("sqlite: set authorizer: %w", err) } return nil } func (c *Conn) releaseAuthorizer() { authorizers.mu.Lock() delete(authorizers.m, c.conn) authorizers.mu.Unlock() } var authorizers struct { mu sync.RWMutex m map[uintptr]Authorizer // sqlite3* -> Authorizer } func authTrampoline(tls *libc.TLS, conn uintptr, op int32, cArg1, cArg2, cDB, cTrigger uintptr) int32 { authorizers.mu.RLock() auth := authorizers.m[conn] authorizers.mu.RUnlock() return int32(auth.Authorize(Action{ op: OpType(op), arg1: libc.GoString(cArg1), arg2: libc.GoString(cArg2), database: libc.GoString(cDB), trigger: libc.GoString(cTrigger), })) } // AuthorizeFunc is a function that implements Authorizer. type AuthorizeFunc func(Action) AuthResult // Authorize calls f. func (f AuthorizeFunc) Authorize(action Action) AuthResult { return f(action) } // AuthResult is the result of a call to an Authorizer. The zero value is // AuthResultOK. type AuthResult int32 // Possible return values from Authorize. const ( // AuthResultOK allows the SQL statement to be compiled. AuthResultOK AuthResult = lib.SQLITE_OK // AuthResultDeny causes the entire SQL statement to be rejected with an error. AuthResultDeny AuthResult = lib.SQLITE_DENY // AuthResultIgnore disallows the specific action but allow the SQL statement // to continue to be compiled. For OpRead, this substitutes a NULL for the // column value. For OpDelete, the DELETE operation proceeds but the truncate // optimization is disabled and all rows are deleted individually. AuthResultIgnore AuthResult = lib.SQLITE_IGNORE ) // String returns the C constant name of the result. func (result AuthResult) String() string { switch result { case AuthResultOK: return "SQLITE_OK" case AuthResultDeny: return "SQLITE_DENY" case AuthResultIgnore: return "SQLITE_IGNORE" default: return fmt.Sprintf("AuthResult(%d)", int32(result)) } } // Action represents an action to be authorized. type Action struct { op OpType arg1 string arg2 string database string trigger string } // Mapping of argument position to concept at: // https://sqlite.org/c3ref/c_alter_table.html // Type returns the type of action being authorized. func (action Action) Type() OpType { return action.op } // Accessor returns the name of the inner-most trigger or view that is // responsible for the access attempt or the empty string if this access attempt // is directly from top-level SQL code. func (action Action) Accessor() string { return action.trigger } // Database returns the name of the database (e.g. "main", "temp", etc.) this // action affects or the empty string if not applicable. func (action Action) Database() string { switch action.op { case OpDetach, OpAlterTable: return action.arg1 default: return action.database } } // Index returns the name of the index this action affects or the empty string // if not applicable. func (action Action) Index() string { switch action.op { case OpCreateIndex, OpCreateTempIndex, OpDropIndex, OpDropTempIndex, OpReindex: return action.arg1 default: return "" } } // Table returns the name of the table this action affects or the empty string // if not applicable. func (action Action) Table() string { switch action.op { case OpCreateTable, OpCreateTempTable, OpDelete, OpDropTable, OpDropTempTable, OpInsert, OpRead, OpUpdate, OpAnalyze, OpCreateVTable, OpDropVTable: return action.arg1 case OpCreateIndex, OpCreateTempIndex, OpCreateTempTrigger, OpCreateTrigger, OpDropIndex, OpDropTempIndex, OpDropTempTrigger, OpDropTrigger, OpAlterTable: return action.arg2 default: return "" } } // Trigger returns the name of the trigger this action affects or the empty // string if not applicable. func (action Action) Trigger() string { switch action.op { case OpCreateTempTrigger, OpCreateTrigger, OpDropTempTrigger, OpDropTrigger: return action.arg1 default: return "" } } // View returns the name of the view this action affects or the empty string // if not applicable. func (action Action) View() string { switch action.op { case OpCreateTempView, OpCreateView, OpDropTempView, OpDropView: return action.arg1 default: return "" } } // Pragma returns the name of the action's PRAGMA command or the empty string // if the action does not represent a PRAGMA command. // See https://sqlite.org/pragma.html#toc for a list of possible values. func (action Action) Pragma() string { if action.op != OpPragma { return "" } return action.arg1 } // PragmaArg returns the argument to the PRAGMA command or the empty string if // the action does not represent a PRAGMA command or the PRAGMA command does not // take an argument. func (action Action) PragmaArg() string { if action.op != OpPragma { return "" } return action.arg2 } // Column returns the name of the column this action affects or the empty string // if not applicable. For OpRead actions, this will return the empty string if a // table is referenced but no column values are extracted from that table // (e.g. a query like "SELECT COUNT(*) FROM tab"). func (action Action) Column() string { switch action.op { case OpRead, OpUpdate: return action.arg2 default: return "" } } // Operation returns one of "BEGIN", "COMMIT", "RELEASE", or "ROLLBACK" for a // transaction or savepoint statement or the empty string otherwise. func (action Action) Operation() string { switch action.op { case OpTransaction, OpSavepoint: return action.arg1 default: return "" } } // File returns the name of the file being ATTACHed or the empty string if the // action does not represent an ATTACH DATABASE statement. func (action Action) File() string { if action.op != OpAttach { return "" } return action.arg1 } // Module returns the module name given to the virtual table statement or the // empty string if the action does not represent a CREATE VIRTUAL TABLE or // DROP VIRTUAL TABLE statement. func (action Action) Module() string { switch action.op { case OpCreateVTable, OpDropVTable: return action.arg2 default: return "" } } // Savepoint returns the name given to the SAVEPOINT statement or the empty // string if the action does not represent a SAVEPOINT statement. func (action Action) Savepoint() string { if action.op != OpSavepoint { return "" } return action.arg2 } // String returns a debugging representation of the action. func (action Action) String() string { sb := new(strings.Builder) sb.WriteString(action.op.String()) params := []struct { name, value string }{ {"database", action.Database()}, {"file", action.File()}, {"trigger", action.Trigger()}, {"index", action.Index()}, {"table", action.Table()}, {"view", action.View()}, {"module", action.Module()}, {"column", action.Column()}, {"operation", action.Operation()}, {"savepoint", action.Savepoint()}, {"pragma", action.Pragma()}, {"arg", action.PragmaArg()}, } for _, p := range params { if p.value != "" { sb.WriteString(" ") sb.WriteString(p.name) sb.WriteString(":") sb.WriteString(p.value) } } return sb.String() }