| --Copyright 2021 The casbin Authors. All Rights Reserved. |
| -- |
| --Licensed under the Apache License, Version 2.0 (the "License"); |
| --you may not use this file except in compliance with the License. |
| --You may obtain a copy of the License at |
| -- |
| -- http://www.apache.org/licenses/LICENSE-2.0 |
| -- |
| --Unless required by applicable law or agreed to in writing, software |
| --distributed under the License is distributed on an "AS IS" BASIS, |
| --WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| --See the License for the specific language governing permissions and |
| --limitations under the License. |
| |
| local Adapter = require("src/persist/Adapter") |
| |
| --[[ |
| * DatabaseAdapter is the SQL adapter for Casbin. It stores the policy in the |
| * usual Casbin table layout (ptype, v0 .. v5) so it is compatible with the |
| * tables written by casbin adapters of the other languages. |
| * |
| * The adapter does not talk to a database itself, it talks to a "driver": |
| * any object with a query() method, which is exactly the shape of the |
| * OpenResty database clients used inside APISIX. |
| * |
| * driver:query(sql) -> rows, err |
| * |
| * A SELECT must return an array of rows, each row a table with the ptype |
| * and v0 .. v5 fields. Any other statement may return anything truthy. |
| * A failure is reported either by raising an error or, like the OpenResty |
| * clients do, by returning nil/false plus an error message; both reach the |
| * enforcer. |
| * |
| * Example with lua-resty-mysql: |
| * |
| * local mysql = require("resty.mysql") |
| * local db = mysql:new() |
| * db:connect({host = "127.0.0.1", database = "casbin", ...}) |
| * |
| * local a = DatabaseAdapter:new(db, {dialect = "mysql", |
| * escapeLiteral = ngx.quote_sql_str}) |
| * local e = Enforcer:new("model.conf", a) |
| * |
| * Example with pgmoon: |
| * |
| * local pg = require("pgmoon").new({host = "127.0.0.1", ...}) |
| * pg:connect() |
| * |
| * local a = DatabaseAdapter:new(pg, {dialect = "postgres", |
| * escapeLiteral = function(v) |
| * return pg:escape_literal(v) |
| * end}) |
| ]] |
| local DatabaseAdapter = {} |
| setmetatable(DatabaseAdapter, Adapter) |
| |
| -- The number of value columns (v0 .. v5), the Casbin standard. |
| local COLUMN_COUNT = 6 |
| |
| --[[ |
| * escapeAnsi quotes a value the way the SQL standard does, by doubling the |
| * single quotes. It is correct for PostgreSQL and SQLite. |
| ]] |
| local function escapeAnsi(value) |
| return "'" .. (tostring(value):gsub("'", "''")) .. "'" |
| end |
| |
| --[[ |
| * escapeMySQL quotes a value the way MySQL does, with backslash escapes. |
| * MySQL treats a backslash inside a string literal as an escape character |
| * unless NO_BACKSLASH_ESCAPES is set, so it must be escaped as well. |
| ]] |
| local function escapeMySQL(value) |
| local s = tostring(value):gsub("\\", "\\\\"):gsub("'", "\\'") |
| return "'" .. s .. "'" |
| end |
| |
| local escapers = { |
| mysql = escapeMySQL, |
| mariadb = escapeMySQL, |
| postgres = escapeAnsi, |
| postgresql = escapeAnsi, |
| sqlite = escapeAnsi, |
| sqlite3 = escapeAnsi, |
| } |
| |
| local createTableSQL = { |
| mysql = "CREATE TABLE IF NOT EXISTS %s (" .. |
| "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, " .. |
| "ptype VARCHAR(100), v0 VARCHAR(100), v1 VARCHAR(100), v2 VARCHAR(100), " .. |
| "v3 VARCHAR(100), v4 VARCHAR(100), v5 VARCHAR(100))", |
| postgres = "CREATE TABLE IF NOT EXISTS %s (" .. |
| "id SERIAL PRIMARY KEY, " .. |
| "ptype VARCHAR(100), v0 VARCHAR(100), v1 VARCHAR(100), v2 VARCHAR(100), " .. |
| "v3 VARCHAR(100), v4 VARCHAR(100), v5 VARCHAR(100))", |
| sqlite = "CREATE TABLE IF NOT EXISTS %s (" .. |
| "id INTEGER PRIMARY KEY AUTOINCREMENT, " .. |
| "ptype TEXT, v0 TEXT, v1 TEXT, v2 TEXT, v3 TEXT, v4 TEXT, v5 TEXT)", |
| } |
| createTableSQL.mariadb = createTableSQL.mysql |
| createTableSQL.postgresql = createTableSQL.postgres |
| createTableSQL.sqlite3 = createTableSQL.sqlite |
| |
| --[[ |
| * failed tells whether a driver reported a failure through the |
| * "return nil/false, err" convention used by the OpenResty clients. |
| ]] |
| local function failed(ok, err) |
| return ok == false or (ok == nil and err ~= nil) |
| end |
| |
| --[[ |
| * isBlank tells whether a column read back from the database carries no |
| * value. Drivers spell SQL NULL differently: pgmoon omits the field, while |
| * lua-resty-mysql returns the ngx.null sentinel, which is neither a string |
| * nor a number. The empty string counts as blank too, because the Casbin |
| * adapters of the other languages leave the unused columns empty rather |
| * than NULL, and their tables have to stay readable here. |
| ]] |
| local function isBlank(value) |
| local t = type(value) |
| if t == "number" then |
| return false |
| end |
| return t ~= "string" or value == "" |
| end |
| |
| --[[ |
| * DatabaseAdapter:new(driver, options) returns a new DatabaseAdapter. |
| * |
| * @param driver an object with a query(sql) method, see the notes above. |
| * @param options an optional table: |
| * tableName the policy table, "casbin_rule" by default. |
| * dialect "mysql", "postgres" or "sqlite", "mysql" by default. |
| * It only selects the default quoting and the DDL of |
| * createTable(). |
| * escapeLiteral function(value) returning a quoted SQL literal. Pass |
| * the one of your driver (ngx.quote_sql_str for |
| * lua-resty-mysql, pg:escape_literal for pgmoon) when |
| * you can, it is always the most accurate. |
| * orderBy column used to keep the rule order stable across |
| * reloads, "id" by default. Set it to false if your |
| * table has no such column. |
| * filtered set it to true to start out filtered, so that the |
| * enforcer does not load the whole table on creation |
| * and waits for your loadFilteredPolicy() call. |
| ]] |
| function DatabaseAdapter:new(driver, options) |
| if not driver or type(driver.query) ~= "function" then |
| error("DatabaseAdapter needs a driver with a query() method.") |
| end |
| |
| options = options or {} |
| |
| local dialect = string.lower(options.dialect or "mysql") |
| if not escapers[dialect] then |
| error("Unsupported dialect: " .. tostring(options.dialect)) |
| end |
| |
| local o = {} |
| setmetatable(o, self) |
| self.__index = self |
| |
| o.driver = driver |
| o.dialect = dialect |
| o.tableName = options.tableName or "casbin_rule" |
| o.escapeLiteral = options.escapeLiteral or escapers[dialect] |
| if options.orderBy == nil then |
| o.orderBy = "id" |
| else |
| o.orderBy = options.orderBy |
| end |
| o.isFiltered = options.filtered == true |
| |
| return o |
| end |
| |
| --[[ |
| * exec runs a statement on the driver and normalizes the outcome, so that a |
| * driver raising an error and a driver returning "nil, err" look the same |
| * to the rest of the adapter. |
| * |
| * @param sql the statement. |
| * @return the driver result on success, or false plus the error message. |
| ]] |
| function DatabaseAdapter:exec(sql) |
| local status, res, err = pcall(self.driver.query, self.driver, sql) |
| |
| if status == false then |
| return false, tostring(res) |
| end |
| |
| if failed(res, err) then |
| return false, tostring(err or "unknown error") |
| end |
| |
| return res |
| end |
| |
| --[[ |
| * createTable creates the policy table when it does not exist yet. It is a |
| * convenience for simple deployments, a schema managed elsewhere works just |
| * as well. |
| ]] |
| function DatabaseAdapter:createTable() |
| return self:exec(string.format(createTableSQL[self.dialect], self.tableName)) |
| end |
| |
| --[[ |
| * rowToRule turns a database row into a policy rule. Casbin stores a rule |
| * of n values in the first n columns, so the rule ends at the first column |
| * that carries no value. |
| ]] |
| local function rowToRule(row) |
| local rule = {} |
| for i = 1, COLUMN_COUNT do |
| local value = row["v" .. (i - 1)] |
| if isBlank(value) then |
| break |
| end |
| table.insert(rule, tostring(value)) |
| end |
| return rule |
| end |
| |
| --[[ |
| * addRuleToModel puts a rule read from the database into the model. Rows of |
| * a ptype the model does not declare are skipped, the same way the file |
| * adapter skips policy lines it cannot place. |
| ]] |
| local function addRuleToModel(model, ptype, rule) |
| if type(ptype) ~= "string" or ptype == "" then |
| return |
| end |
| |
| local sec = ptype:sub(1, 1) |
| if not model.model[sec] then return end |
| if not model.model[sec][ptype] then return end |
| |
| model:addPolicy(sec, ptype, rule) |
| end |
| |
| --[[ |
| * loadPolicy loads all policy rules from the database. |
| ]] |
| function DatabaseAdapter:loadPolicy(model) |
| local sql = "SELECT ptype, v0, v1, v2, v3, v4, v5 FROM " .. self.tableName |
| if self.orderBy then |
| sql = sql .. " ORDER BY " .. self.orderBy |
| end |
| |
| local rows, err = self:exec(sql) |
| if failed(rows, err) then |
| return false, err |
| end |
| |
| for _, row in ipairs(rows or {}) do |
| addRuleToModel(model, row.ptype, rowToRule(row)) |
| end |
| |
| self.isFiltered = false |
| return true |
| end |
| |
| --[[ |
| * sectionClause builds the WHERE clause selecting the rules of one section |
| * that match the filter values. An empty value matches anything, as in the |
| * file adapter. |
| ]] |
| function DatabaseAdapter:sectionClause(sec, values) |
| -- The section letter is a literal of ours, so it needs no quoting. |
| local clause = "ptype LIKE '" .. sec .. "%'" |
| |
| for i, value in ipairs(values or {}) do |
| if i > COLUMN_COUNT then |
| error("Filter of section " .. sec .. " has more than " .. COLUMN_COUNT .. " values.") |
| end |
| if value ~= "" then |
| clause = clause .. " AND v" .. (i - 1) .. " = " .. self.escapeLiteral(value) |
| end |
| end |
| |
| return clause |
| end |
| |
| --[[ |
| * loadFilteredPolicy loads only the policy rules that match the filter, so |
| * that a big policy table does not have to be held in memory as a whole. |
| * |
| * @param model the model. |
| * @param filter a table with the P and G value lists, like the filter of |
| * the file adapter. A nil filter loads everything. |
| ]] |
| function DatabaseAdapter:loadFilteredPolicy(model, filter) |
| if filter == nil then |
| return self:loadPolicy(model) |
| end |
| |
| if not filter.P or not filter.G then |
| error("Invalid filter type.") |
| end |
| |
| local sql = "SELECT ptype, v0, v1, v2, v3, v4, v5 FROM " .. self.tableName .. |
| " WHERE (" .. self:sectionClause("p", filter.P) .. ")" .. |
| " OR (" .. self:sectionClause("g", filter.G) .. ")" |
| if self.orderBy then |
| sql = sql .. " ORDER BY " .. self.orderBy |
| end |
| |
| local rows, err = self:exec(sql) |
| if failed(rows, err) then |
| return false, err |
| end |
| |
| for _, row in ipairs(rows or {}) do |
| addRuleToModel(model, row.ptype, rowToRule(row)) |
| end |
| |
| self.isFiltered = true |
| return true |
| end |
| |
| --[[ |
| * insertValues renders the "('p', 'alice', ...)" tuple of one rule. Columns |
| * the rule does not fill are stored as NULL, which is what rowToRule reads |
| * back as "the rule ends here". |
| ]] |
| function DatabaseAdapter:insertValues(ptype, rule) |
| if #rule > COLUMN_COUNT then |
| error("A policy rule cannot have more than " .. COLUMN_COUNT .. " values.") |
| end |
| |
| local values = {self.escapeLiteral(ptype)} |
| for i = 1, COLUMN_COUNT do |
| if rule[i] == nil then |
| table.insert(values, "NULL") |
| else |
| table.insert(values, self.escapeLiteral(rule[i])) |
| end |
| end |
| |
| return "(" .. table.concat(values, ", ") .. ")" |
| end |
| |
| --[[ |
| * insertSQL renders an INSERT of one or more rules of the same ptype. |
| ]] |
| function DatabaseAdapter:insertSQL(ptype, rules) |
| local tuples = {} |
| for _, rule in ipairs(rules) do |
| table.insert(tuples, self:insertValues(ptype, rule)) |
| end |
| |
| return "INSERT INTO " .. self.tableName .. |
| " (ptype, v0, v1, v2, v3, v4, v5) VALUES " .. table.concat(tuples, ", ") |
| end |
| |
| --[[ |
| * ruleClause builds the WHERE clause identifying exactly one rule: the |
| * columns the rule does not fill must be empty, otherwise a shorter rule |
| * would also match a longer one sharing its prefix. Empty is matched as |
| * both NULL and "", so that rows written by a Casbin adapter of another |
| * language can be deleted here as well. |
| ]] |
| function DatabaseAdapter:ruleClause(ptype, rule) |
| local clause = "ptype = " .. self.escapeLiteral(ptype) |
| |
| for i = 1, COLUMN_COUNT do |
| if rule[i] == nil then |
| clause = clause .. " AND (v" .. (i - 1) .. " IS NULL OR v" .. (i - 1) .. " = '')" |
| else |
| clause = clause .. " AND v" .. (i - 1) .. " = " .. self.escapeLiteral(rule[i]) |
| end |
| end |
| |
| return clause |
| end |
| |
| --[[ |
| * savePolicy saves all policy rules to the database, replacing what is |
| * stored there. Wrap the call in a transaction of your own if you need the |
| * replacement to be atomic. |
| ]] |
| function DatabaseAdapter:savePolicy(model) |
| local ok, err = self:exec("DELETE FROM " .. self.tableName) |
| if failed(ok, err) then |
| return false, err |
| end |
| |
| for _, sec in ipairs({"p", "g"}) do |
| if model.model[sec] then |
| for ptype, ast in pairs(model.model[sec]) do |
| if #ast.policy > 0 then |
| ok, err = self:exec(self:insertSQL(ptype, ast.policy)) |
| if failed(ok, err) then |
| return false, err |
| end |
| end |
| end |
| end |
| end |
| |
| return true |
| end |
| |
| --[[ |
| * addPolicy adds a policy rule to the database. |
| ]] |
| function DatabaseAdapter:addPolicy(sec, ptype, rule) |
| local ok, err = self:exec(self:insertSQL(ptype, {rule})) |
| if failed(ok, err) then |
| return false, err |
| end |
| return true |
| end |
| |
| --[[ |
| * addPolicies adds policy rules to the database. |
| ]] |
| function DatabaseAdapter:addPolicies(sec, ptype, rules) |
| if not rules or #rules == 0 then |
| return true |
| end |
| |
| local ok, err = self:exec(self:insertSQL(ptype, rules)) |
| if failed(ok, err) then |
| return false, err |
| end |
| return true |
| end |
| |
| --[[ |
| * removePolicy removes a policy rule from the database. |
| ]] |
| function DatabaseAdapter:removePolicy(sec, ptype, rule) |
| local ok, err = self:exec("DELETE FROM " .. self.tableName .. |
| " WHERE " .. self:ruleClause(ptype, rule)) |
| if failed(ok, err) then |
| return false, err |
| end |
| return true |
| end |
| |
| --[[ |
| * removePolicies removes policy rules from the database. |
| ]] |
| function DatabaseAdapter:removePolicies(sec, ptype, rules) |
| for _, rule in ipairs(rules or {}) do |
| local ok, err = self:removePolicy(sec, ptype, rule) |
| if failed(ok, err) then |
| return false, err |
| end |
| end |
| return true |
| end |
| |
| --[[ |
| * filteredClause builds the WHERE clause of removeFilteredPolicy. As |
| * everywhere in Casbin, fieldIndex is zero based: 0 stands for column v0. |
| ]] |
| function DatabaseAdapter:filteredClause(ptype, fieldIndex, fieldValues) |
| if fieldIndex < 0 or fieldIndex + #fieldValues > COLUMN_COUNT then |
| error("The field index is out of range.") |
| end |
| |
| local clause = "ptype = " .. self.escapeLiteral(ptype) |
| |
| for i, value in ipairs(fieldValues) do |
| if value ~= "" then |
| clause = clause .. " AND v" .. (fieldIndex + i - 1) .. " = " .. self.escapeLiteral(value) |
| end |
| end |
| |
| return clause |
| end |
| |
| --[[ |
| * removeFilteredPolicy removes the policy rules that match the filter from |
| * the database. |
| ]] |
| function DatabaseAdapter:removeFilteredPolicy(sec, ptype, fieldIndex, fieldValues) |
| local ok, err = self:exec("DELETE FROM " .. self.tableName .. |
| " WHERE " .. self:filteredClause(ptype, fieldIndex, fieldValues)) |
| if failed(ok, err) then |
| return false, err |
| end |
| return true |
| end |
| |
| --[[ |
| * updatePolicy replaces a policy rule in the database. |
| ]] |
| function DatabaseAdapter:updatePolicy(sec, ptype, oldRule, newRule) |
| local ok, err = self:removePolicy(sec, ptype, oldRule) |
| if failed(ok, err) then |
| return false, err |
| end |
| |
| return self:addPolicy(sec, ptype, newRule) |
| end |
| |
| --[[ |
| * updatePolicies replaces several policy rules in the database. |
| ]] |
| function DatabaseAdapter:updatePolicies(sec, ptype, oldRules, newRules) |
| local ok, err = self:removePolicies(sec, ptype, oldRules) |
| if failed(ok, err) then |
| return false, err |
| end |
| |
| return self:addPolicies(sec, ptype, newRules) |
| end |
| |
| --[[ |
| * updateFilteredPolicies deletes the rules matching the filter and adds the |
| * new rules in their place. |
| ]] |
| function DatabaseAdapter:updateFilteredPolicies(sec, ptype, newRules, fieldIndex, fieldValues) |
| local ok, err = self:removeFilteredPolicy(sec, ptype, fieldIndex, fieldValues) |
| if failed(ok, err) then |
| return false, err |
| end |
| |
| return self:addPolicies(sec, ptype, newRules) |
| end |
| |
| return DatabaseAdapter |