Masking: Add MaskingRules
MXS-910: Initial implementation of rule handling, now only the parsing of JSON file and the building of corresponding objects. Rudimentary testing.
This commit is contained in:
parent
0b4c379539
commit
dc87663c95
@ -1,9 +1,18 @@
|
||||
add_library(masking SHARED
|
||||
maskingfilter.cc
|
||||
maskingfiltersession.cc
|
||||
)
|
||||
if (JANSSON_FOUND)
|
||||
add_library(masking SHARED
|
||||
maskingfilter.cc
|
||||
maskingfiltersession.cc
|
||||
maskingrules.cc
|
||||
)
|
||||
|
||||
target_link_libraries(masking maxscale-common)
|
||||
set_target_properties(masking PROPERTIES VERSION "1.0.0")
|
||||
set_target_properties(masking PROPERTIES LINK_FLAGS -Wl,-z,defs)
|
||||
install_module(masking experimental)
|
||||
target_link_libraries(masking maxscale-common jansson)
|
||||
set_target_properties(masking PROPERTIES VERSION "1.0.0")
|
||||
set_target_properties(masking PROPERTIES LINK_FLAGS -Wl,-z,defs)
|
||||
install_module(masking experimental)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "No Jansson libraries found, not building cache filter.")
|
||||
endif()
|
||||
|
704
server/modules/filter/masking/maskingrules.cc
Normal file
704
server/modules/filter/masking/maskingrules.cc
Normal file
@ -0,0 +1,704 @@
|
||||
/*
|
||||
* Copyright (c) 2016 MariaDB Corporation Ab
|
||||
*
|
||||
* Use of this software is governed by the Business Source License included
|
||||
* in the LICENSE.TXT file and at www.mariadb.com/bsl.
|
||||
*
|
||||
* Change Date: 2019-07-01
|
||||
*
|
||||
* On the date above, in accordance with the Business Source License, use
|
||||
* of this software will be governed by version 2 or later of the General
|
||||
* Public License.
|
||||
*/
|
||||
|
||||
#include "maskingrules.hh"
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <maxscale/debug.h>
|
||||
#include <maxscale/jansson.hh>
|
||||
#include <maxscale/mysql_utils.h>
|
||||
#include <maxscale/pcre2.hh>
|
||||
#include <maxscale/utils.hh>
|
||||
|
||||
using std::auto_ptr;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
using std::tr1::shared_ptr;
|
||||
using maxscale::Closer;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
static const char KEY_APPLIES_TO[] = "applies_to";
|
||||
static const char KEY_COLUMN[] = "column";
|
||||
static const char KEY_DATABASE[] = "database";
|
||||
static const char KEY_EXEMPTED[] = "exempted";
|
||||
static const char KEY_FILL[] = "fill";
|
||||
static const char KEY_REPLACE[] = "replace";
|
||||
static const char KEY_RULES[] = "rules";
|
||||
static const char KEY_TABLE[] = "table";
|
||||
static const char KEY_VALUE[] = "value";
|
||||
static const char KEY_WITH[] = "with";
|
||||
|
||||
/**
|
||||
* @class AccountVerbatim
|
||||
*
|
||||
* Implementation of @c MaskingRules::Rule::Account that compares user and
|
||||
* host names verbatim, that is, without regexp matching.
|
||||
*/
|
||||
class AccountVerbatim : public MaskingRules::Rule::Account
|
||||
{
|
||||
public:
|
||||
~AccountVerbatim()
|
||||
{
|
||||
}
|
||||
|
||||
static shared_ptr<MaskingRules::Rule::Account> create(const string& user, const string& host)
|
||||
{
|
||||
return shared_ptr<MaskingRules::Rule::Account>(new AccountVerbatim(user, host));
|
||||
}
|
||||
|
||||
string user() const
|
||||
{
|
||||
return m_user;
|
||||
}
|
||||
|
||||
string host() const
|
||||
{
|
||||
return m_host;
|
||||
}
|
||||
|
||||
bool matches(const char* zUser, const char* zHost) const
|
||||
{
|
||||
ss_dassert(zUser);
|
||||
ss_dassert(zHost);
|
||||
|
||||
return
|
||||
(m_user.empty() || (m_user == zUser)) &&
|
||||
(m_host.empty() || (m_host == zHost));
|
||||
}
|
||||
|
||||
private:
|
||||
AccountVerbatim(const string& user, const string& host)
|
||||
: m_user(user)
|
||||
, m_host(host)
|
||||
{
|
||||
}
|
||||
|
||||
AccountVerbatim(const AccountVerbatim&);
|
||||
AccountVerbatim& operator = (const AccountVerbatim&);
|
||||
|
||||
private:
|
||||
string m_user;
|
||||
string m_host;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @class AccountRegexp
|
||||
*
|
||||
* Implementation of @c MaskingRules::Rule::Account that compares user names
|
||||
* verbatim, that is, without regexp matching, and host names using regexp
|
||||
* matching.
|
||||
*/
|
||||
class AccountRegexp : public MaskingRules::Rule::Account
|
||||
{
|
||||
public:
|
||||
~AccountRegexp()
|
||||
{
|
||||
pcre2_match_data_free(m_pData);
|
||||
pcre2_code_free(m_pCode);
|
||||
}
|
||||
|
||||
static shared_ptr<MaskingRules::Rule::Account> create(const string& user, const string& host)
|
||||
{
|
||||
shared_ptr<MaskingRules::Rule::Account> sAccount;
|
||||
|
||||
int errcode;
|
||||
PCRE2_SIZE erroffset;
|
||||
pcre2_code* pCode = pcre2_compile((PCRE2_SPTR)host.c_str(), PCRE2_ZERO_TERMINATED, 0,
|
||||
&errcode, &erroffset, NULL);
|
||||
|
||||
if (pCode)
|
||||
{
|
||||
Closer<pcre2_code*> code(pCode);
|
||||
|
||||
pcre2_match_data* pData = pcre2_match_data_create_from_pattern(pCode, NULL);
|
||||
|
||||
if (pData)
|
||||
{
|
||||
Closer<pcre2_match_data*> data(pData);
|
||||
|
||||
sAccount = shared_ptr<AccountRegexp>(new AccountRegexp(user, host, pCode, pData));
|
||||
|
||||
// Ownership of pCode and pData has been moved to the
|
||||
// AccountRegexp instance.
|
||||
data.release();
|
||||
code.release();
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("PCRE2 match data creation failed. Most likely due to a "
|
||||
"lack of available memory.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PCRE2_UCHAR errbuf[512];
|
||||
pcre2_get_error_message(errcode, errbuf, sizeof(errbuf));
|
||||
MXS_ERROR("Regex compilation failed at %d for regex '%s': %s",
|
||||
(int)erroffset, host.c_str(), errbuf);
|
||||
}
|
||||
|
||||
return sAccount;
|
||||
}
|
||||
|
||||
string user() const
|
||||
{
|
||||
return m_user;
|
||||
}
|
||||
|
||||
string host() const
|
||||
{
|
||||
return m_host;
|
||||
}
|
||||
|
||||
bool matches(const char* zUser, const char* zHost) const
|
||||
{
|
||||
ss_dassert(zUser);
|
||||
ss_dassert(zHost);
|
||||
|
||||
return
|
||||
(m_user.empty() || (m_user == zUser)) &&
|
||||
pcre2_match(m_pCode, (PCRE2_SPTR)zHost, 0, 0, 0, m_pData, NULL) >= 0;
|
||||
}
|
||||
|
||||
private:
|
||||
AccountRegexp(const string& user,
|
||||
const string& host,
|
||||
pcre2_code* pCode,
|
||||
pcre2_match_data* pData)
|
||||
: m_user(user)
|
||||
, m_host(host)
|
||||
, m_pCode(pCode)
|
||||
, m_pData(pData)
|
||||
{
|
||||
}
|
||||
|
||||
AccountRegexp(const AccountRegexp&);
|
||||
AccountRegexp& operator = (const AccountRegexp&);
|
||||
|
||||
private:
|
||||
string m_user;
|
||||
string m_host;
|
||||
pcre2_code* m_pCode;
|
||||
pcre2_match_data* m_pData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create MaxskingRules::Rule::Account instance
|
||||
*
|
||||
* @param zAccount The account name as specified in the JSON rules file.
|
||||
*
|
||||
* @return Either an AccountVerbatim or AccountRegexp, depending on whether
|
||||
* the provided account name contains wildcards or not.
|
||||
*/
|
||||
shared_ptr<MaskingRules::Rule::Account> create_account(const char* zAccount)
|
||||
{
|
||||
shared_ptr<MaskingRules::Rule::Account> sAccount;
|
||||
|
||||
size_t len = strlen(zAccount);
|
||||
char account[len + 1];
|
||||
strcpy(account, zAccount);
|
||||
|
||||
char* zAt = strchr(account, '@');
|
||||
char* zUser = account;
|
||||
char* zHost = NULL;
|
||||
|
||||
if (zAt)
|
||||
{
|
||||
*zAt = 0;
|
||||
zHost = zAt + 1;
|
||||
}
|
||||
|
||||
if (mxs_mysql_trim_quotes(zUser))
|
||||
{
|
||||
char pcre_host[2 * len + 1]; // Surely enough
|
||||
|
||||
mxs_mysql_name_kind_t rv = MXS_MYSQL_NAME_WITHOUT_WILDCARD;
|
||||
|
||||
if (zHost)
|
||||
{
|
||||
if (mxs_mysql_trim_quotes(zHost))
|
||||
{
|
||||
rv = mxs_mysql_name_to_pcre(pcre_host, zHost, MXS_PCRE_QUOTE_WILDCARD);
|
||||
|
||||
if (rv == MXS_MYSQL_NAME_WITH_WILDCARD)
|
||||
{
|
||||
zHost = pcre_host;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("Could not trim quotes from host part of %s.", zAccount);
|
||||
zHost = NULL;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
zHost = const_cast<char*>("");
|
||||
}
|
||||
|
||||
if (zHost)
|
||||
{
|
||||
if (rv == MXS_MYSQL_NAME_WITH_WILDCARD)
|
||||
{
|
||||
sAccount = AccountRegexp::create(zUser, zHost);
|
||||
}
|
||||
else
|
||||
{
|
||||
sAccount = AccountVerbatim::create(zUser, zHost);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("Could not trim quotes from user part of %s.", zAccount);
|
||||
}
|
||||
|
||||
return sAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of account names into a vector of Account instances.
|
||||
*
|
||||
* @param zName The key of the JSON array we are processing (error reporting).
|
||||
* @param pString A JSON array of account names.
|
||||
* @param accounts Vector of Account instances, to be filled by this function.
|
||||
*
|
||||
* @return True, if all account names could be converted, false otherwise.
|
||||
*/
|
||||
bool get_accounts(const char* zName,
|
||||
json_t* pStrings,
|
||||
vector<shared_ptr<MaskingRules::Rule::Account> >& accounts)
|
||||
{
|
||||
ss_dassert(json_is_array(pStrings));
|
||||
|
||||
bool success = true;
|
||||
|
||||
size_t n = json_array_size(pStrings);
|
||||
size_t i = 0;
|
||||
|
||||
while (success && (i < n))
|
||||
{
|
||||
json_t* pString = json_array_get(pStrings, i);
|
||||
ss_dassert(pString);
|
||||
|
||||
if (json_is_string(pString))
|
||||
{
|
||||
shared_ptr<MaskingRules::Rule::Account> sAccount = create_account(json_string_value(pString));
|
||||
|
||||
if (sAccount)
|
||||
{
|
||||
accounts.push_back(sAccount);
|
||||
}
|
||||
else
|
||||
{
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("An element in a '%s' array is not a string.", zName);
|
||||
success = false;
|
||||
}
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MaskingRules::Rule instance
|
||||
*
|
||||
* @param pColumn A JSON string containing a column name.
|
||||
* @param pTable A JSON string containing a table name, or NULL.
|
||||
* @param pDatabase A JSON string containing a table name, or NULL.
|
||||
* @param pValue A JSON string representing the 'value' of a 'with' object from the rules file.
|
||||
* @param pFill A JSON string representing the 'fill' of a 'with' object from the rules file.
|
||||
* @param pApplies_to A JSON array representing the 'applies_to' account names.
|
||||
* @param pExempted A JSON array representing the 'exempted' account names.
|
||||
*
|
||||
* @return A Rule instance or NULL in case of error.
|
||||
*/
|
||||
auto_ptr<MaskingRules::Rule> create_rule_from_elements(json_t* pColumn,
|
||||
json_t* pTable,
|
||||
json_t* pDatabase,
|
||||
json_t* pValue,
|
||||
json_t* pFill,
|
||||
json_t* pApplies_to,
|
||||
json_t* pExempted)
|
||||
{
|
||||
ss_dassert(pColumn && json_is_string(pColumn));
|
||||
ss_dassert(!pTable || json_is_string(pTable));
|
||||
ss_dassert(!pDatabase || json_is_string(pDatabase));
|
||||
ss_dassert(pValue || pFill);
|
||||
ss_dassert(!pValue || json_is_string(pValue));
|
||||
ss_dassert(!pFill || json_is_string(pFill));
|
||||
ss_dassert(!pApplies_to || json_is_array(pApplies_to));
|
||||
ss_dassert(!pExempted || json_is_array(pExempted));
|
||||
|
||||
auto_ptr<MaskingRules::Rule> sRule;
|
||||
|
||||
string column(json_string_value(pColumn));
|
||||
string table(pTable ? json_string_value(pTable) : "");
|
||||
string database(pDatabase ? json_string_value(pDatabase) : "");
|
||||
string value(pValue ? json_string_value(pValue) : "");
|
||||
string fill(pFill ? json_string_value(pFill) : "");
|
||||
|
||||
bool ok = true;
|
||||
vector<shared_ptr<MaskingRules::Rule::Account> > applies_to;
|
||||
vector<shared_ptr<MaskingRules::Rule::Account> > exempted;
|
||||
|
||||
if (ok && pApplies_to)
|
||||
{
|
||||
ok = get_accounts(KEY_APPLIES_TO, pApplies_to, applies_to);
|
||||
}
|
||||
|
||||
if (ok && pExempted)
|
||||
{
|
||||
ok = get_accounts(KEY_EXEMPTED, pExempted, exempted);
|
||||
}
|
||||
|
||||
if (ok)
|
||||
{
|
||||
sRule = auto_ptr<MaskingRules::Rule>(new MaskingRules::Rule(column, table, database,
|
||||
value, fill,
|
||||
applies_to, exempted));
|
||||
}
|
||||
|
||||
return sRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MaskingRules::Rule instance
|
||||
*
|
||||
* @param pReplace A JSON object representing 'replace' of a rule from the rules file.
|
||||
* @param pWith A JSON object representing 'with' of a rule from the rules file.
|
||||
* @param pApplies_to A JSON object representing 'applies_to' of a rule from the rules file.
|
||||
* @param pExempted A JSON object representing 'exempted' of a rule from the rules file.
|
||||
*
|
||||
* @return A Rule instance or NULL in case of error.
|
||||
*/
|
||||
auto_ptr<MaskingRules::Rule> create_rule_from_elements(json_t* pReplace,
|
||||
json_t* pWith,
|
||||
json_t* pApplies_to,
|
||||
json_t* pExempted)
|
||||
{
|
||||
ss_dassert(pReplace && json_is_object(pReplace));
|
||||
ss_dassert(pWith && json_is_object(pWith));
|
||||
ss_dassert(!pApplies_to || json_is_array(pApplies_to));
|
||||
ss_dassert(!pExempted || json_is_array(pExempted));
|
||||
|
||||
auto_ptr<MaskingRules::Rule> sRule;
|
||||
|
||||
json_t* pDatabase = json_object_get(pReplace, KEY_DATABASE);
|
||||
json_t* pTable = json_object_get(pReplace, KEY_TABLE);
|
||||
json_t* pColumn = json_object_get(pReplace, KEY_COLUMN);
|
||||
|
||||
// A column is mandatory; both table and database are optional.
|
||||
if ((pColumn && json_is_string(pColumn)) &&
|
||||
(!pTable || json_is_string(pTable)) &&
|
||||
(!pDatabase || json_is_string(pDatabase)))
|
||||
{
|
||||
json_t* pValue = json_object_get(pWith, KEY_VALUE);
|
||||
json_t* pFill = json_object_get(pWith, KEY_FILL);
|
||||
|
||||
if ((pValue || pFill) &&
|
||||
(!pValue || json_is_string(pValue)) &&
|
||||
(!pFill || json_is_string(pFill)))
|
||||
{
|
||||
sRule = create_rule_from_elements(pColumn, pTable, pDatabase,
|
||||
pValue, pFill,
|
||||
pApplies_to, pExempted);
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("The '%s' object of a masking rule does not have either '%s' "
|
||||
"or '%s' as keys, or their values are not strings.",
|
||||
KEY_WITH, KEY_VALUE, KEY_FILL);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("The '%s' object of a masking rule does not have a '%s' key, or "
|
||||
"the values of that key and/or possible '%s' and '%s' keys are "
|
||||
"not strings.",
|
||||
KEY_REPLACE, KEY_COLUMN, KEY_TABLE, KEY_DATABASE);
|
||||
}
|
||||
|
||||
return sRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all MaskingRules::Rule instances
|
||||
*
|
||||
* @param pRules A JSON array representing 'rules' from the rules file.
|
||||
* @param rules Vector where corresponding Rule instances will be pushed.
|
||||
*
|
||||
* @return True, if all rules could be created.
|
||||
*/
|
||||
bool create_rules_from_array(json_t* pRules, vector<shared_ptr<MaskingRules::Rule> >& rules)
|
||||
{
|
||||
ss_dassert(json_is_array(pRules));
|
||||
|
||||
bool parsed = true;
|
||||
|
||||
size_t n = json_array_size(pRules);
|
||||
size_t i = 0;
|
||||
|
||||
while (parsed && (i < n))
|
||||
{
|
||||
json_t* pRule = json_array_get(pRules, i);
|
||||
ss_dassert(pRule);
|
||||
|
||||
if (json_is_object(pRule))
|
||||
{
|
||||
auto_ptr<MaskingRules::Rule> sRule = MaskingRules::Rule::create_from(pRule);
|
||||
|
||||
if (sRule.get())
|
||||
{
|
||||
rules.push_back(shared_ptr<MaskingRules::Rule>(sRule.release()));
|
||||
}
|
||||
else
|
||||
{
|
||||
parsed = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("Element %lu of the '%s' array is not an object.", i, KEY_RULES);
|
||||
parsed = false;
|
||||
}
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all MaskingRules::Rule instances
|
||||
*
|
||||
* @param pRoo A JSON object, representing the rules file.
|
||||
* @param rules Vector where all Rule instances will be pushed.
|
||||
*
|
||||
* @return True, if all rules could be created.
|
||||
*/
|
||||
bool create_rules_from_root(json_t* pRoot, vector<shared_ptr<MaskingRules::Rule> >& rules)
|
||||
{
|
||||
bool parsed = false;
|
||||
json_t* pRules = json_object_get(pRoot, KEY_RULES);
|
||||
|
||||
if (pRules)
|
||||
{
|
||||
if (json_is_array(pRules))
|
||||
{
|
||||
parsed = create_rules_from_array(pRules, rules);
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("The masking rules object contains a `%s` key, but it is not an array.", KEY_RULES);
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// MaskingRules::Rule::Account
|
||||
//
|
||||
|
||||
MaskingRules::Rule::Account::Account()
|
||||
{
|
||||
}
|
||||
|
||||
MaskingRules::Rule::Account::~Account()
|
||||
{
|
||||
}
|
||||
|
||||
//
|
||||
// MaskingRules::Rule
|
||||
//
|
||||
|
||||
MaskingRules::Rule::Rule(const std::string& column,
|
||||
const std::string& table,
|
||||
const std::string& database,
|
||||
const std::string& value,
|
||||
const std::string& fill,
|
||||
const std::vector<SAccount>& applies_to,
|
||||
const std::vector<SAccount>& exempted)
|
||||
: m_column(column)
|
||||
, m_table(table)
|
||||
, m_database(database)
|
||||
, m_value(value)
|
||||
, m_fill(fill)
|
||||
, m_applies_to(applies_to)
|
||||
, m_exempted(exempted)
|
||||
{
|
||||
}
|
||||
|
||||
MaskingRules::Rule::~Rule()
|
||||
{
|
||||
}
|
||||
|
||||
//static
|
||||
auto_ptr<MaskingRules::Rule> MaskingRules::Rule::create_from(json_t* pRule)
|
||||
{
|
||||
ss_dassert(json_is_object(pRule));
|
||||
|
||||
auto_ptr<MaskingRules::Rule> sRule;
|
||||
|
||||
json_t* pReplace = json_object_get(pRule, KEY_REPLACE);
|
||||
json_t* pWith = json_object_get(pRule, KEY_WITH);
|
||||
json_t* pApplies_to = json_object_get(pRule, KEY_APPLIES_TO);
|
||||
json_t* pExempted = json_object_get(pRule, KEY_EXEMPTED);
|
||||
|
||||
if (pReplace && pWith)
|
||||
{
|
||||
bool ok = true;
|
||||
|
||||
if (!json_is_object(pReplace))
|
||||
{
|
||||
MXS_ERROR("A masking rule contains a '%s' key, but the value is not an object.",
|
||||
KEY_REPLACE);
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!json_is_object(pWith))
|
||||
{
|
||||
MXS_ERROR("A masking rule contains a '%s' key, but the value is not an object.",
|
||||
KEY_WITH);
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (pApplies_to && !json_is_array(pApplies_to))
|
||||
{
|
||||
MXS_ERROR("A masking rule contains a '%s' key, but the value is not an array.",
|
||||
KEY_APPLIES_TO);
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (pExempted && !json_is_array(pExempted))
|
||||
{
|
||||
MXS_ERROR("A masking rule contains a '%s' key, but the value is not an array.",
|
||||
KEY_EXEMPTED);
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (ok)
|
||||
{
|
||||
sRule = create_rule_from_elements(pReplace, pWith, pApplies_to, pExempted);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("A masking rule does not contain a '%s' and/or a '%s' key.", KEY_REPLACE, KEY_WITH);
|
||||
}
|
||||
|
||||
return sRule;
|
||||
}
|
||||
|
||||
//
|
||||
// MaskingRules
|
||||
//
|
||||
|
||||
MaskingRules::MaskingRules(json_t* pRoot, const std::vector<SRule>& rules)
|
||||
: m_pRoot(pRoot)
|
||||
, m_rules(rules)
|
||||
{
|
||||
json_incref(m_pRoot);
|
||||
}
|
||||
|
||||
MaskingRules::~MaskingRules()
|
||||
{
|
||||
json_decref(m_pRoot);
|
||||
}
|
||||
|
||||
//static
|
||||
auto_ptr<MaskingRules> MaskingRules::load(const char* zPath)
|
||||
{
|
||||
auto_ptr<MaskingRules> sRules;
|
||||
|
||||
FILE* pFile = fopen(zPath, "r");
|
||||
|
||||
if (pFile)
|
||||
{
|
||||
Closer<FILE*> file(pFile);
|
||||
|
||||
json_error_t error;
|
||||
json_t* pRoot = json_loadf(file.get(), JSON_DISABLE_EOF_CHECK, &error);
|
||||
|
||||
if (pRoot)
|
||||
{
|
||||
Closer<json_t*> root(pRoot);
|
||||
|
||||
sRules = create_from(root.get());
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("Loading rules file failed: (%s:%d:%d): %s",
|
||||
zPath, error.line, error.column, error.text);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
char errbuf[MXS_STRERROR_BUFLEN];
|
||||
|
||||
MXS_ERROR("Could not open rules file %s for reading: %s",
|
||||
zPath, strerror_r(errno, errbuf, sizeof(errbuf)));
|
||||
}
|
||||
|
||||
return sRules;
|
||||
}
|
||||
|
||||
//static
|
||||
auto_ptr<MaskingRules> MaskingRules::parse(const char* zJson)
|
||||
{
|
||||
auto_ptr<MaskingRules> sRules;
|
||||
|
||||
json_error_t error;
|
||||
json_t* pRoot = json_loads(zJson, JSON_DISABLE_EOF_CHECK, &error);
|
||||
|
||||
if (pRoot)
|
||||
{
|
||||
Closer<json_t*> root(pRoot);
|
||||
|
||||
sRules = create_from(root.get());
|
||||
}
|
||||
else
|
||||
{
|
||||
MXS_ERROR("Parsing rules failed: (%d:%d): %s",
|
||||
error.line, error.column, error.text);
|
||||
}
|
||||
|
||||
return sRules;
|
||||
}
|
||||
|
||||
//static
|
||||
std::auto_ptr<MaskingRules> MaskingRules::create_from(json_t* pRoot)
|
||||
{
|
||||
auto_ptr<MaskingRules> sRules;
|
||||
|
||||
vector<SRule> rules;
|
||||
|
||||
if (create_rules_from_root(pRoot, rules))
|
||||
{
|
||||
sRules = auto_ptr<MaskingRules>(new MaskingRules(pRoot, rules));
|
||||
}
|
||||
|
||||
return sRules;
|
||||
}
|
166
server/modules/filter/masking/maskingrules.hh
Normal file
166
server/modules/filter/masking/maskingrules.hh
Normal file
@ -0,0 +1,166 @@
|
||||
#pragma once
|
||||
/*
|
||||
* Copyright (c) 2016 MariaDB Corporation Ab
|
||||
*
|
||||
* Use of this software is governed by the Business Source License included
|
||||
* in the LICENSE.TXT file and at www.mariadb.com/bsl.
|
||||
*
|
||||
* Change Date: 2019-07-01
|
||||
*
|
||||
* On the date above, in accordance with the Business Source License, use
|
||||
* of this software will be governed by version 2 or later of the General
|
||||
* Public License.
|
||||
*/
|
||||
|
||||
#include <maxscale/cppdefs.hh>
|
||||
#include <memory>
|
||||
#include <tr1/memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <jansson.h>
|
||||
|
||||
/**
|
||||
* @class MaskingRules
|
||||
*
|
||||
* MaskingRules abstracts the rules of a masking filter.
|
||||
*/
|
||||
class MaskingRules
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @class Rule
|
||||
*
|
||||
* A Rule represents a single masking rule.
|
||||
*/
|
||||
class Rule
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @class Account
|
||||
*
|
||||
* An instance of this class is capable of answering the question
|
||||
* whether the current user is subject to masking.
|
||||
*/
|
||||
class Account
|
||||
{
|
||||
public:
|
||||
Account();
|
||||
virtual ~Account();
|
||||
|
||||
virtual std::string user() const = 0;
|
||||
virtual std::string host() const = 0;
|
||||
|
||||
/**
|
||||
* Is a user subject to masking?
|
||||
*
|
||||
* @param zUser The name of the user.
|
||||
* @param zHost The host of the user.
|
||||
*
|
||||
* @return True, if the data should be masked.
|
||||
*/
|
||||
virtual bool matches(const char* zUser, const char* zHost) const = 0;
|
||||
};
|
||||
|
||||
typedef std::tr1::shared_ptr<Account> SAccount;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param column The column value from the json file.
|
||||
* @param table The table value from the json file.
|
||||
* @param database The database value from the json file.
|
||||
* @param value The value value from the json file.
|
||||
* @param fill The file value from the json file.
|
||||
* @param applies_to Account instances corresponding to the
|
||||
* accounts listed in 'applies_to' in the json file.
|
||||
* @param exempted Account instances corresponding to the
|
||||
* accounts listed in 'exempted' in the json file.
|
||||
*/
|
||||
Rule(const std::string& column,
|
||||
const std::string& table,
|
||||
const std::string& database,
|
||||
const std::string& value,
|
||||
const std::string& fill,
|
||||
const std::vector<SAccount>& applies_to,
|
||||
const std::vector<SAccount>& exempted);
|
||||
~Rule();
|
||||
|
||||
const std::string& column() const { return m_column; }
|
||||
const std::string& table() const { return m_table; }
|
||||
const std::string& database() const { return m_database; }
|
||||
const std::string& value() const { return m_value; }
|
||||
const std::string& fill() const { return m_fill; }
|
||||
const std::vector<SAccount>& applies_to() const { return m_applies_to; }
|
||||
const std::vector<SAccount>& exempted() const { return m_exempted; }
|
||||
|
||||
/**
|
||||
* Create a Rule instance
|
||||
*
|
||||
* @param pRule A json object corresponding to a single
|
||||
* rule in the rules json file.
|
||||
*
|
||||
* @return A Rule instance or NULL.
|
||||
*/
|
||||
static std::auto_ptr<Rule> create_from(json_t* pRule);
|
||||
|
||||
private:
|
||||
Rule(const Rule&);
|
||||
Rule& operator = (const Rule&);
|
||||
|
||||
private:
|
||||
std::string m_column;
|
||||
std::string m_table;
|
||||
std::string m_database;
|
||||
std::string m_value;
|
||||
std::string m_fill;
|
||||
std::vector<SAccount> m_applies_to;
|
||||
std::vector<SAccount> m_exempted;
|
||||
};
|
||||
|
||||
~MaskingRules();
|
||||
|
||||
/**
|
||||
* Load rules
|
||||
*
|
||||
* @param zPath Path to rules file.
|
||||
*
|
||||
* @return A rules object, or NULL if the rules could not be loaded.
|
||||
* or parsed.
|
||||
*/
|
||||
static std::auto_ptr<MaskingRules> load(const char* zPath);
|
||||
|
||||
/**
|
||||
* Parse rules
|
||||
*
|
||||
* @param zPath Path to rules file.
|
||||
*
|
||||
* @return A rules object, or NULL if the rules could not be parsed.
|
||||
*/
|
||||
static std::auto_ptr<MaskingRules> parse(const char* zJson);
|
||||
|
||||
/**
|
||||
* Create rules from JSON object.
|
||||
*
|
||||
* @param pRoot Pointer to JSON object.
|
||||
*
|
||||
* @return A rules object, or NULL if the rules could not be created.
|
||||
*/
|
||||
static std::auto_ptr<MaskingRules> create_from(json_t* pRoot);
|
||||
|
||||
typedef std::tr1::shared_ptr<Rule> SRule;
|
||||
const std::vector<SRule>& rules() const
|
||||
{
|
||||
return m_rules;
|
||||
}
|
||||
|
||||
private:
|
||||
MaskingRules(json_t* pRoot, const std::vector<SRule>& rules);
|
||||
|
||||
private:
|
||||
MaskingRules(const MaskingRules&);
|
||||
MaskingRules& operator = (const MaskingRules&);
|
||||
|
||||
private:
|
||||
json_t* m_pRoot;
|
||||
std::vector<SRule> m_rules;
|
||||
};
|
6
server/modules/filter/masking/test/CMakeLists.txt
Normal file
6
server/modules/filter/masking/test/CMakeLists.txt
Normal file
@ -0,0 +1,6 @@
|
||||
include_directories(..)
|
||||
|
||||
add_executable(masking_testrules testrules.cc ../maskingrules.cc)
|
||||
target_link_libraries(masking_testrules maxscale-common jansson)
|
||||
|
||||
add_test(TestMasking_rules masking_testrules)
|
248
server/modules/filter/masking/test/testrules.cc
Normal file
248
server/modules/filter/masking/test/testrules.cc
Normal file
@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright (c) 2016 MariaDB Corporation Ab
|
||||
*
|
||||
* Use of this software is governed by the Business Source License included
|
||||
* in the LICENSE.TXT file and at www.mariadb.com/bsl.
|
||||
*
|
||||
* Change Date: 2019-07-01
|
||||
*
|
||||
* On the date above, in accordance with the Business Source License, use
|
||||
* of this software will be governed by version 2 or later of the General
|
||||
* Public License.
|
||||
*/
|
||||
|
||||
#include "maskingrules.hh"
|
||||
#include <iostream>
|
||||
#include <maxscale/debug.h>
|
||||
|
||||
using namespace std;
|
||||
using namespace std::tr1;
|
||||
|
||||
const char valid_minimal[] =
|
||||
"{"
|
||||
" \"rules\": ["
|
||||
" {"
|
||||
" \"replace\": { "
|
||||
" \"column\": \"a\" "
|
||||
" },"
|
||||
" \"with\": {"
|
||||
" \"value\": \"blah\" "
|
||||
" }"
|
||||
" }"
|
||||
" ]"
|
||||
"}";
|
||||
|
||||
const char valid_maximal[] =
|
||||
"{"
|
||||
" \"rules\": ["
|
||||
" {"
|
||||
" \"replace\": { "
|
||||
" \"column\": \"a\", "
|
||||
" \"table\": \"b\", "
|
||||
" \"database\": \"c\" "
|
||||
" },"
|
||||
" \"with\": {"
|
||||
" \"value\": \"blah\", "
|
||||
" \"fill\": \"blah\" "
|
||||
" },"
|
||||
" \"applies_to\": ["
|
||||
" \"'alice'@'host'\","
|
||||
" \"'bob'@'%'\","
|
||||
" \"'cecil'@'%.123.45.2'\""
|
||||
" ],"
|
||||
" \"exempted\": ["
|
||||
" \"'admin'\""
|
||||
" ]"
|
||||
" }"
|
||||
" ]"
|
||||
"}";
|
||||
|
||||
// Neither "replace", nor "with".
|
||||
const char invalid1[] =
|
||||
"{"
|
||||
" \"rules\": ["
|
||||
" {"
|
||||
" \"applies_to\": ["
|
||||
" \"'alice'@'host'\","
|
||||
" \"'bob'@'%'\""
|
||||
" ],"
|
||||
" \"exempted\": ["
|
||||
" \"'admin'\""
|
||||
" ]"
|
||||
" }"
|
||||
" ]"
|
||||
"}";
|
||||
|
||||
// No "column" in "replace"
|
||||
const char invalid2[] =
|
||||
"{"
|
||||
" \"rules\": ["
|
||||
" {"
|
||||
" \"replace\": { "
|
||||
" },"
|
||||
" \"with\": { "
|
||||
" \"value\": \"blah\", "
|
||||
" }"
|
||||
" }"
|
||||
" ]"
|
||||
"}";
|
||||
|
||||
// No "value" or "fill" in "with"
|
||||
const char invalid3[] =
|
||||
"{"
|
||||
" \"rules\": ["
|
||||
" {"
|
||||
" \"replace\": { "
|
||||
" \"column\": \"a\", "
|
||||
" },"
|
||||
" \"with\": {"
|
||||
" },"
|
||||
" }"
|
||||
" ]"
|
||||
"}";
|
||||
|
||||
struct rule_test
|
||||
{
|
||||
const char* zJson;
|
||||
bool valid;
|
||||
} rule_tests[] =
|
||||
{
|
||||
{ valid_minimal, true },
|
||||
{ valid_maximal, true },
|
||||
{ invalid1, false },
|
||||
{ invalid2, false },
|
||||
{ invalid3, false },
|
||||
};
|
||||
|
||||
const size_t nRule_tests = (sizeof(rule_tests) / sizeof(rule_tests[0]));
|
||||
|
||||
int test_parsing()
|
||||
{
|
||||
int rc = EXIT_SUCCESS;
|
||||
|
||||
for (size_t i = 0; i < nRule_tests; i++)
|
||||
{
|
||||
const rule_test& test = rule_tests[i];
|
||||
|
||||
auto_ptr<MaskingRules> sRules = MaskingRules::parse(test.zJson);
|
||||
|
||||
if ((sRules.get() && !test.valid) || (!sRules.get() && test.valid))
|
||||
{
|
||||
rc = EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Valid, lot's of users.
|
||||
const char valid_users[] =
|
||||
"{"
|
||||
" \"rules\": ["
|
||||
" {"
|
||||
" \"replace\": { "
|
||||
" \"column\": \"a\" "
|
||||
" },"
|
||||
" \"with\": {"
|
||||
" \"value\": \"blah\" "
|
||||
" },"
|
||||
" \"applies_to\": ["
|
||||
" \"'alice'@'host'\","
|
||||
" \"'bob'@'%'\","
|
||||
" \"'cecil'@'%.123.45.2'\","
|
||||
" \"'david'\","
|
||||
" \"@'host'\""
|
||||
" ],"
|
||||
" \"exempted\": ["
|
||||
" \"'admin'\""
|
||||
" ]"
|
||||
" }"
|
||||
" ]"
|
||||
"}";
|
||||
|
||||
struct expected_account
|
||||
{
|
||||
const char* zUser;
|
||||
const char* zHost;
|
||||
} expected_accounts[] =
|
||||
{
|
||||
{
|
||||
"alice",
|
||||
"host",
|
||||
},
|
||||
{
|
||||
"bob",
|
||||
".*"
|
||||
},
|
||||
{
|
||||
"cecil",
|
||||
".*\\.123\\.45\\.2"
|
||||
},
|
||||
{
|
||||
"david",
|
||||
""
|
||||
},
|
||||
{
|
||||
"",
|
||||
"host"
|
||||
}
|
||||
};
|
||||
|
||||
const size_t nExpected_accounts = (sizeof(expected_accounts)/sizeof(expected_accounts[0]));
|
||||
|
||||
int test_account_handling()
|
||||
{
|
||||
int rc = EXIT_SUCCESS;
|
||||
|
||||
auto_ptr<MaskingRules> sRules = MaskingRules::parse(valid_users);
|
||||
ss_dassert(sRules.get());
|
||||
|
||||
const vector<shared_ptr<MaskingRules::Rule> >& rules = sRules->rules();
|
||||
ss_dassert(rules.size() == 1);
|
||||
|
||||
shared_ptr<MaskingRules::Rule> sRule = rules[0];
|
||||
|
||||
const vector<shared_ptr<MaskingRules::Rule::Account> >& accounts = sRule->applies_to();
|
||||
ss_dassert(accounts.size() == nExpected_accounts);
|
||||
|
||||
int j = 0;
|
||||
for (vector<shared_ptr<MaskingRules::Rule::Account> >::const_iterator i = accounts.begin();
|
||||
i != accounts.end();
|
||||
++i)
|
||||
{
|
||||
const expected_account& account = expected_accounts[j];
|
||||
|
||||
string user = (*i)->user();
|
||||
|
||||
if (user != account.zUser)
|
||||
{
|
||||
cout << j << ": Expected \"" << account.zUser << "\", got \"" << user << "\"." << endl;
|
||||
rc = EXIT_FAILURE;
|
||||
}
|
||||
|
||||
string host = (*i)->host();
|
||||
|
||||
if (host != account.zHost)
|
||||
{
|
||||
cout << j << ": Expected \"" << account.zHost << "\", got \"" << host << "\"." << endl;
|
||||
rc = EXIT_FAILURE;
|
||||
}
|
||||
|
||||
++j;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
int rc = EXIT_SUCCESS;
|
||||
|
||||
if (mxs_log_init(NULL, ".", MXS_LOG_TARGET_DEFAULT))
|
||||
{
|
||||
rc = (test_parsing() == EXIT_FAILURE) ? EXIT_FAILURE : EXIT_SUCCESS;
|
||||
rc = (test_account_handling() == EXIT_FAILURE) ? EXIT_FAILURE : EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user