Skip to content

[CLIENT-3161] Add client config option to validate keys passed into client config and policy dictionaries #729

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 41 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
68b6290
doc
juliannguyen4 Feb 20, 2025
c28a503
fix
juliannguyen4 Feb 20, 2025
d27d6a4
add more
juliannguyen4 Feb 20, 2025
5389751
test formatting
juliannguyen4 Feb 20, 2025
338deac
add code
juliannguyen4 Feb 20, 2025
2b5a713
add err msg
juliannguyen4 Feb 20, 2025
63775fb
add link
juliannguyen4 Feb 20, 2025
c9a0fb3
policy ex
juliannguyen4 Feb 20, 2025
5ab110a
add
juliannguyen4 Feb 20, 2025
e6d341d
fix
juliannguyen4 Feb 20, 2025
5ae575c
improve error msgs, fix
juliannguyen4 Feb 20, 2025
cfdf891
oops
juliannguyen4 Feb 20, 2025
3deefe3
Merge remote-tracking branch 'origin/dev' into CLIENT-3161-validate-d…
juliannguyen4 Apr 9, 2025
8049d4e
wip
juliannguyen4 Apr 9, 2025
af35ccd
fix
juliannguyen4 Apr 9, 2025
399aa5d
Merge remote-tracking branch 'origin/dev' into CLIENT-3161-validate-d…
juliannguyen4 Apr 10, 2025
03701fb
validate logic
juliannguyen4 Apr 10, 2025
9513e4f
dont need to
juliannguyen4 Apr 11, 2025
bcac0f7
fix
juliannguyen4 Apr 11, 2025
42c7fd9
wrong pointer type
juliannguyen4 Apr 11, 2025
6cd1143
fix
juliannguyen4 Apr 11, 2025
107f67b
fix
juliannguyen4 Apr 11, 2025
b0f93df
fix
juliannguyen4 Apr 11, 2025
82138f8
fix
juliannguyen4 Apr 11, 2025
8c78bd8
fix
juliannguyen4 Apr 14, 2025
07b7d8d
Merge remote-tracking branch 'origin/dev' into CLIENT-3161-validate-d…
juliannguyen4 Apr 23, 2025
0fa9f87
clear up
juliannguyen4 Apr 23, 2025
9316849
rename to str to make consistent
juliannguyen4 Apr 23, 2025
9adb09c
clear up
juliannguyen4 Apr 23, 2025
e4cff79
cleanup
juliannguyen4 Apr 23, 2025
8a523a6
make optional
juliannguyen4 Apr 23, 2025
bea8686
f
juliannguyen4 Apr 23, 2025
09aa9d3
Add basic test
juliannguyen4 Apr 23, 2025
1c39ac4
revert
juliannguyen4 Apr 23, 2025
de01c62
fix
juliannguyen4 Apr 23, 2025
72dae54
fix
juliannguyen4 Apr 23, 2025
333b249
doesnt match with docs
juliannguyen4 Apr 24, 2025
22b1ca5
wip
juliannguyen4 Apr 24, 2025
1f0daf8
Merge remote-tracking branch 'origin/dev' into CLIENT-3161-validate-d…
juliannguyen4 May 5, 2025
246d5a5
Revert "wip"
juliannguyen4 May 5, 2025
facd84d
remove policy documentation
juliannguyen4 May 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ max-line-length = 120
extend-ignore = E203
filename =
./aerospike_helpers/**/*.py,
./test/**/*.py
./test/new_tests/**/*.py
31 changes: 31 additions & 0 deletions doc/aerospike.rst
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,37 @@ Only the `hosts` key is required; the rest of the keys are optional.
.. hlist::
:columns: 1

* **validate_keys** (:class:`bool`)
(Optional) Validate keys passed into this config dictionary.

If a key that is undefined in this documentation gets passed to a config dictionary:

* If this option is set to :py:obj:`True`, :py:class:`~aerospike.exception.ParamError` will be raised.
* If this option is set to :py:obj:`False`, the key will be ignored and the client does not raise an
exception in response to the invalid key.

Default: :py:obj:`False`

Invalid client config example:

.. code-block:: python

import aerospike

config = {
"validate_keys": True,
"hosts": [
("127.0.0.1", 3000)
],
# The correct key is "user", but "username" may be used by accident
"username": "user",
"password": "password"
}
# This call will raise a ParamError from aerospike.exception
# Exception message should be:
# "username" is an invalid client config dictionary key
client = aerospike.client(config)

* **hosts** (:class:`list`)
A list of tuples identifying a node (or multiple nodes) in the cluster.

Expand Down
91 changes: 90 additions & 1 deletion src/main/aerospike.c
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,78 @@ static struct type_name_to_creation_method py_module_types[] = {
{"Transaction", AerospikeTransaction_Ready},
};

const char *const client_config_valid_keys[] = {"lua",
"tls",
"hosts",
"shm",
"serialization",
"policies",
"thread_pool_size",
"max_threads",
"min_conns_per_node",
"max_conns_per_node",
"max_error_rate",
"error_rate_window",
"connect_timeout",
"use_shared_connection",
"send_bool_as",
"compression_threshold",
"tend_interval",
"cluster_name",
"strict_types",
"rack_aware",
"rack_id",
"rack_ids",
"use_services_alternate",
"max_socket_idle",
"fail_if_not_connected",
"user",
"password",
"validate_keys",
NULL};

const char *const client_config_shm_valid_keys[] = {
"shm_max_nodes",
"max_nodes",
"shm_max_namespaces",
"max_namespaces",
"shm_takeover_threshold_sec",
"takeover_threshold_sec"
"shm_key",
NULL};

// Return NULL if an exception is raised
// Returns strong reference to new Python dictionary
static PyObject *py_set_new_from_str_list(const char *const *str_list)
{
PyObject *py_valid_keys = PySet_New(NULL);
if (py_valid_keys == NULL) {
goto error;
}

const char *const *curr_str_ref = str_list;
while (*curr_str_ref) {
PyObject *py_str = PyUnicode_FromString(*curr_str_ref);
if (py_str == NULL) {
goto CLEANUP_SET_ON_ERROR;
}

int result = PySet_Add(py_valid_keys, py_str);
Py_DECREF(py_str);
if (result == -1) {
goto CLEANUP_SET_ON_ERROR;
}
curr_str_ref++;
}

return py_valid_keys;

CLEANUP_SET_ON_ERROR:
Py_DECREF(py_valid_keys);
error:
return NULL;
}

PyMODINIT_FUNC PyInit_aerospike(void)
{
static struct PyModuleDef moduledef = {
Expand All @@ -575,13 +647,30 @@ PyMODINIT_FUNC PyInit_aerospike(void)

Aerospike_Enable_Default_Logging();

// just use a Python set so we don't need to implement a hashset in C
// The C client does not have a public API for a hashset yet
// Time complexity of set should be constant on avg:
// https://wiki.python.org/moin/TimeComplexity
PyObject *py_client_config_valid_keys =
py_set_new_from_str_list(client_config_valid_keys);
if (py_client_config_valid_keys == NULL) {
goto MODULE_CLEANUP_ON_ERROR;
}
// TODO: add to C struct to make private?
int retval =
PyModule_AddObject(py_aerospike_module, "__client_config_valid_keys",
py_client_config_valid_keys);
if (retval == -1) {
Py_DECREF(py_client_config_valid_keys);
goto MODULE_CLEANUP_ON_ERROR;
}

py_global_hosts = PyDict_New();
if (py_global_hosts == NULL) {
goto MODULE_CLEANUP_ON_ERROR;
}

unsigned long i = 0;
int retval;
for (i = 0; i < sizeof(py_module_types) / sizeof(py_module_types[0]); i++) {
PyTypeObject *(*py_type_ready_func)(void) =
py_module_types[i].pytype_ready_method;
Expand Down
76 changes: 75 additions & 1 deletion src/main/client/type.c
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,80 @@ static int AerospikeClient_Type_Init(AerospikeClient *self, PyObject *args,
as_config config;
as_config_init(&config);

bool lua_user_path = false;
// Very first thing to check before validating config keys
PyObject *py_validate_keys_str = PyUnicode_FromString("validate_keys");
if (py_validate_keys_str == NULL) {
goto RAISE_EXCEPTION_WITHOUT_AS_ERROR;
}

int validate_keys = 0;
PyObject *py_validate_keys =
PyDict_GetItemWithError(py_config, py_validate_keys_str);
Py_DECREF(py_validate_keys_str);
if (py_validate_keys) {
if (!PyBool_Check(py_validate_keys)) {
as_error_update(&constructor_err, AEROSPIKE_ERR_PARAM,
"config[\"validate_keys\"] must be a boolean");
goto RAISE_EXCEPTION_USING_AS_ERROR;
}
validate_keys = PyObject_IsTrue(py_validate_keys);
if (validate_keys == -1) {
goto RAISE_EXCEPTION_WITHOUT_AS_ERROR;
}
}

if (validate_keys) {
PyObject *py_aerospike_module_name = PyUnicode_FromString("aerospike");
if (!py_aerospike_module_name) {
goto RAISE_EXCEPTION_WITHOUT_AS_ERROR;
}
PyObject *py_aerospike_module =
PyImport_GetModule(py_aerospike_module_name);
Py_DECREF(py_aerospike_module_name);
if (py_aerospike_module == NULL) {
if (PyErr_Occurred()) {
goto RAISE_EXCEPTION_WITHOUT_AS_ERROR;
}
else {
as_error_update(
&constructor_err, AEROSPIKE_ERR_CLIENT,
"Unable to check if the client config keys are valid.");
goto RAISE_EXCEPTION_USING_AS_ERROR;
}
}

// TODO: define constant in a header file
// TODO: user can delete attribute, breaking this code
PyObject *py_valid_keys = PyObject_GetAttrString(
py_aerospike_module, "__client_config_valid_keys");
Py_DECREF(py_aerospike_module);
if (py_valid_keys == NULL) {
goto RAISE_EXCEPTION_WITHOUT_AS_ERROR;
}

Py_ssize_t pos = 0;
PyObject *py_config_key = NULL;
while (PyDict_Next(py_config, &pos, &py_config_key, NULL)) {
int res = PySet_Contains(py_valid_keys, py_config_key);
const char *config_key = PyUnicode_AsUTF8(py_config_key);
if (res == -1) {
Py_DECREF(py_valid_keys);
goto RAISE_EXCEPTION_WITHOUT_AS_ERROR;
}
else if (res == 0) {
as_error_update(
&constructor_err, AEROSPIKE_ERR_PARAM,
"\"%s\" is an invalid client config dictionary key",
config_key);
goto RAISE_EXCEPTION_USING_AS_ERROR;
}
// Config key is valid
}

Py_DECREF(py_valid_keys);
}

bool lua_user_path = false;
PyObject *py_lua = PyDict_GetItemString(py_config, "lua");
if (py_lua && PyDict_Check(py_lua)) {

Expand Down Expand Up @@ -1109,7 +1181,9 @@ static int AerospikeClient_Type_Init(AerospikeClient *self, PyObject *args,
break;
}

RAISE_EXCEPTION_USING_AS_ERROR:
raise_exception(&constructor_err);
RAISE_EXCEPTION_WITHOUT_AS_ERROR:
return -1;
}

Expand Down
9 changes: 9 additions & 0 deletions test/new_tests/test_invalid_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,12 @@ def test_invalid_remove_policy_types(self, key, value):
subpolicy = {key: value}
with pytest.raises(e.ParamError):
aerospike.client({"hosts": [("localhost", 3000)], "policies": {"remove": subpolicy}})

def test_validate_keys(self):
config = {
"validate_keys": True,
"host": [("127.0.0.1", 3000)]
}
with pytest.raises(e.ParamError) as excinfo:
aerospike.client(config)
assert excinfo.value.msg == '\"host\" is an invalid client config dictionary key'