From dc87663c9562c285bb8e4d2b7f576c25062815c8 Mon Sep 17 00:00:00 2001 From: Johan Wikman Date: Thu, 29 Dec 2016 17:36:00 +0200 Subject: [PATCH] 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. --- server/modules/filter/masking/CMakeLists.txt | 25 +- server/modules/filter/masking/maskingrules.cc | 704 ++++++++++++++++++ server/modules/filter/masking/maskingrules.hh | 166 +++++ .../filter/masking/test/CMakeLists.txt | 6 + .../modules/filter/masking/test/testrules.cc | 248 ++++++ 5 files changed, 1141 insertions(+), 8 deletions(-) create mode 100644 server/modules/filter/masking/maskingrules.cc create mode 100644 server/modules/filter/masking/maskingrules.hh create mode 100644 server/modules/filter/masking/test/CMakeLists.txt create mode 100644 server/modules/filter/masking/test/testrules.cc diff --git a/server/modules/filter/masking/CMakeLists.txt b/server/modules/filter/masking/CMakeLists.txt index 221407789..85a0b2d05 100644 --- a/server/modules/filter/masking/CMakeLists.txt +++ b/server/modules/filter/masking/CMakeLists.txt @@ -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() diff --git a/server/modules/filter/masking/maskingrules.cc b/server/modules/filter/masking/maskingrules.cc new file mode 100644 index 000000000..7fd368822 --- /dev/null +++ b/server/modules/filter/masking/maskingrules.cc @@ -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 +#include +#include +#include +#include +#include +#include + +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 create(const string& user, const string& host) + { + return shared_ptr(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 create(const string& user, const string& host) + { + shared_ptr 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 code(pCode); + + pcre2_match_data* pData = pcre2_match_data_create_from_pattern(pCode, NULL); + + if (pData) + { + Closer data(pData); + + sAccount = shared_ptr(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 create_account(const char* zAccount) +{ + shared_ptr 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(""); + } + + 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 >& 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 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 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 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 > applies_to; + vector > 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(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 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 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 >& 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 sRule = MaskingRules::Rule::create_from(pRule); + + if (sRule.get()) + { + rules.push_back(shared_ptr(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 >& 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& applies_to, + const std::vector& 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::create_from(json_t* pRule) +{ + ss_dassert(json_is_object(pRule)); + + auto_ptr 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& rules) + : m_pRoot(pRoot) + , m_rules(rules) +{ + json_incref(m_pRoot); +} + +MaskingRules::~MaskingRules() +{ + json_decref(m_pRoot); +} + +//static +auto_ptr MaskingRules::load(const char* zPath) +{ + auto_ptr sRules; + + FILE* pFile = fopen(zPath, "r"); + + if (pFile) + { + Closer file(pFile); + + json_error_t error; + json_t* pRoot = json_loadf(file.get(), JSON_DISABLE_EOF_CHECK, &error); + + if (pRoot) + { + Closer 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::parse(const char* zJson) +{ + auto_ptr sRules; + + json_error_t error; + json_t* pRoot = json_loads(zJson, JSON_DISABLE_EOF_CHECK, &error); + + if (pRoot) + { + Closer 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::create_from(json_t* pRoot) +{ + auto_ptr sRules; + + vector rules; + + if (create_rules_from_root(pRoot, rules)) + { + sRules = auto_ptr(new MaskingRules(pRoot, rules)); + } + + return sRules; +} diff --git a/server/modules/filter/masking/maskingrules.hh b/server/modules/filter/masking/maskingrules.hh new file mode 100644 index 000000000..f42a7a615 --- /dev/null +++ b/server/modules/filter/masking/maskingrules.hh @@ -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 +#include +#include +#include +#include +#include + +/** + * @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 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& applies_to, + const std::vector& 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& applies_to() const { return m_applies_to; } + const std::vector& 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 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 m_applies_to; + std::vector 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 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 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 create_from(json_t* pRoot); + + typedef std::tr1::shared_ptr SRule; + const std::vector& rules() const + { + return m_rules; + } + +private: + MaskingRules(json_t* pRoot, const std::vector& rules); + +private: + MaskingRules(const MaskingRules&); + MaskingRules& operator = (const MaskingRules&); + +private: + json_t* m_pRoot; + std::vector m_rules; +}; diff --git a/server/modules/filter/masking/test/CMakeLists.txt b/server/modules/filter/masking/test/CMakeLists.txt new file mode 100644 index 000000000..6cac31090 --- /dev/null +++ b/server/modules/filter/masking/test/CMakeLists.txt @@ -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) diff --git a/server/modules/filter/masking/test/testrules.cc b/server/modules/filter/masking/test/testrules.cc new file mode 100644 index 000000000..3c8f2293a --- /dev/null +++ b/server/modules/filter/masking/test/testrules.cc @@ -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 +#include + +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 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 sRules = MaskingRules::parse(valid_users); + ss_dassert(sRules.get()); + + const vector >& rules = sRules->rules(); + ss_dassert(rules.size() == 1); + + shared_ptr sRule = rules[0]; + + const vector >& accounts = sRule->applies_to(); + ss_dassert(accounts.size() == nExpected_accounts); + + int j = 0; + for (vector >::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; +}