From 856a2f59562ed73e91293b0b816eefc6577d7ed5 Mon Sep 17 00:00:00 2001 From: Sergei Trifonov Date: Thu, 25 Aug 2022 17:24:24 +0200 Subject: [PATCH] Allow to modify constrained settings in readonly mode --- .../settings/constraints-on-settings.md | 28 +++- .../settings/permissions-for-queries.md | 3 +- src/Access/SettingsConstraints.cpp | 122 +++++++++++++++--- src/Access/SettingsConstraints.h | 23 +++- src/Access/SettingsProfileElement.cpp | 4 + src/Access/SettingsProfileElement.h | 4 +- src/Access/UsersConfigAccessStorage.cpp | 4 + src/Core/Settings.h | 2 +- src/Storages/System/StorageSystemSettings.cpp | 15 ++- .../StorageSystemSettingsProfileElements.cpp | 26 +++- 10 files changed, 198 insertions(+), 33 deletions(-) diff --git a/docs/en/operations/settings/constraints-on-settings.md b/docs/en/operations/settings/constraints-on-settings.md index d240fde8ff3..37424ea20cb 100644 --- a/docs/en/operations/settings/constraints-on-settings.md +++ b/docs/en/operations/settings/constraints-on-settings.md @@ -31,7 +31,7 @@ The constraints are defined as the following: ``` If the user tries to violate the constraints an exception is thrown and the setting isn’t changed. -There are supported three types of constraints: `min`, `max`, `readonly`. The `min` and `max` constraints specify upper and lower boundaries for a numeric setting and can be used in combination. The `readonly` constraint specifies that the user cannot change the corresponding setting at all. +There are supported three types of constraints: `min`, `max`, `readonly`. The `min` and `max` constraints specify upper and lower boundaries for a numeric setting and can be used in combination. The `readonly` constraint specifies that the user cannot change the corresponding setting at all. Also for `readonly = 1` mode there are two additional types of constraints: `min_in_readonly` and `max_in_readonly`, see details below. **Example:** Let `users.xml` includes lines: @@ -68,6 +68,32 @@ Code: 452, e.displayText() = DB::Exception: Setting max_memory_usage should not Code: 452, e.displayText() = DB::Exception: Setting force_index_by_date should not be changed. ``` +## Constraints in read-only mode {#settings-readonly-constraints} +Read-only mode is enabled by `readonly` setting: +- `readonly=0`: No read-only restrictions. +- `readonly=1`: Only read queries are allowed and settings cannot be changes unless explicitly allowed. +- `readonly=2`: Only read queries are allowed, but settings can be changed, except for `readonly` setting itself. + +In both read-only modes settings constraints are applied and additionally in `readonly=1` mode some settings changes can be explicitly allowed in the following way: +``` xml + + + + + lower_boundary + + + upper_boundary + + + lower_boundary + upper_boundary + + + + +``` + **Note:** the `default` profile has special handling: all the constraints defined for the `default` profile become the default constraints, so they restrict all the users until they’re overridden explicitly for these users. [Original article](https://clickhouse.com/docs/en/operations/settings/constraints_on_settings/) diff --git a/docs/en/operations/settings/permissions-for-queries.md b/docs/en/operations/settings/permissions-for-queries.md index 668cb9993eb..a5bce92b49f 100644 --- a/docs/en/operations/settings/permissions-for-queries.md +++ b/docs/en/operations/settings/permissions-for-queries.md @@ -36,8 +36,7 @@ After setting `readonly = 1`, the user can’t change `readonly` and `allow_ddl` When using the `GET` method in the [HTTP interface](../../interfaces/http.md), `readonly = 1` is set automatically. To modify data, use the `POST` method. -Setting `readonly = 1` prohibit the user from changing all the settings. There is a way to prohibit the user -from changing only specific settings, for details see [constraints on settings](../../operations/settings/constraints-on-settings.md). +Setting `readonly = 1` prohibit the user from changing all the settings. There is a way to prohibit the user from changing only specific settings. Also there is a way to allow changing only specific settings under `readonly = 1` restrictions. For details see [constraints on settings](../../operations/settings/constraints-on-settings.md). Default value: 0 diff --git a/src/Access/SettingsConstraints.cpp b/src/Access/SettingsConstraints.cpp index 34f2e10dc83..fc8e5e91653 100644 --- a/src/Access/SettingsConstraints.cpp +++ b/src/Access/SettingsConstraints.cpp @@ -81,6 +81,36 @@ bool SettingsConstraints::isReadOnly(std::string_view setting_name) const } +void SettingsConstraints::setMinValueInReadOnly(std::string_view setting_name, const Field & min_value_in_readonly) +{ + getConstraintRef(setting_name).min_value_in_readonly = Settings::castValueUtil(setting_name, min_value_in_readonly); +} + +Field SettingsConstraints::getMinValueInReadOnly(std::string_view setting_name) const +{ + const auto * ptr = tryGetConstraint(setting_name); + if (ptr) + return ptr->min_value_in_readonly; + else + return {}; +} + + +void SettingsConstraints::setMaxValueInReadOnly(std::string_view setting_name, const Field & max_value_in_readonly) +{ + getConstraintRef(setting_name).max_value_in_readonly = Settings::castValueUtil(setting_name, max_value_in_readonly); +} + +Field SettingsConstraints::getMaxValueInReadOnly(std::string_view setting_name) const +{ + const auto * ptr = tryGetConstraint(setting_name); + if (ptr) + return ptr->max_value_in_readonly; + else + return {}; +} + + void SettingsConstraints::set(std::string_view setting_name, const Field & min_value, const Field & max_value, bool read_only) { auto & ref = getConstraintRef(setting_name); @@ -89,7 +119,7 @@ void SettingsConstraints::set(std::string_view setting_name, const Field & min_v ref.read_only = read_only; } -void SettingsConstraints::get(std::string_view setting_name, Field & min_value, Field & max_value, bool & read_only) const +void SettingsConstraints::get(std::string_view setting_name, Field & min_value, Field & max_value, bool & read_only, Field & min_value_in_readonly, Field & max_value_in_readonly) const { const auto * ptr = tryGetConstraint(setting_name); if (ptr) @@ -97,12 +127,16 @@ void SettingsConstraints::get(std::string_view setting_name, Field & min_value, min_value = ptr->min_value; max_value = ptr->max_value; read_only = ptr->read_only; + min_value_in_readonly = ptr->min_value_in_readonly; + max_value_in_readonly = ptr->max_value_in_readonly; } else { min_value = Field{}; max_value = Field{}; read_only = false; + min_value_in_readonly = Field{}; + max_value_in_readonly = Field{}; } } @@ -117,6 +151,10 @@ void SettingsConstraints::merge(const SettingsConstraints & other) constraint.max_value = other_constraint.max_value; if (other_constraint.read_only) constraint.read_only = true; + if (!other_constraint.min_value_in_readonly.isNull()) + constraint.min_value_in_readonly = other_constraint.min_value_in_readonly; + if (!other_constraint.max_value_in_readonly.isNull()) + constraint.max_value_in_readonly = other_constraint.max_value_in_readonly; } } @@ -180,10 +218,8 @@ bool SettingsConstraints::checkImpl(const Settings & current_settings, SettingCh } }; - bool cannot_compare = false; - auto less = [&](const Field & left, const Field & right) + auto less_or_cannot_compare = [=](const Field & left, const Field & right) { - cannot_compare = false; if (reaction == THROW_ON_VIOLATION) return applyVisitor(FieldVisitorAccurateLess{}, left, right); else @@ -194,8 +230,7 @@ bool SettingsConstraints::checkImpl(const Settings & current_settings, SettingCh } catch (...) { - cannot_compare = true; - return false; + return true; } } }; @@ -248,17 +283,10 @@ bool SettingsConstraints::checkImpl(const Settings & current_settings, SettingCh } /** The `readonly` value is understood as follows: - * 0 - everything allowed. - * 1 - only read queries can be made; you can not change the settings. - * 2 - You can only do read queries and you can change the settings, except for the `readonly` setting. + * 0 - no read-only restrictions. + * 1 - only read requests, as well as changing explicitly allowed settings. + * 2 - only read requests, as well as changing settings, except for the `readonly` setting. */ - if (current_settings.readonly == 1) - { - if (reaction == THROW_ON_VIOLATION) - throw Exception("Cannot modify '" + setting_name + "' setting in readonly mode", ErrorCodes::READONLY); - else - return false; - } if (current_settings.readonly > 1 && setting_name == "readonly") { @@ -269,6 +297,58 @@ bool SettingsConstraints::checkImpl(const Settings & current_settings, SettingCh } const Constraint * constraint = tryGetConstraint(setting_name); + if (current_settings.readonly == 1) + { + if (!constraint || (constraint->min_value_in_readonly.isNull() && constraint->max_value_in_readonly.isNull())) { + if (reaction == THROW_ON_VIOLATION) + throw Exception("Cannot modify '" + setting_name + "' setting in readonly mode", ErrorCodes::READONLY); + else + return false; + } + + const Field & min_value = constraint->min_value_in_readonly; + const Field & max_value = constraint->max_value_in_readonly; + if (!min_value.isNull() && !max_value.isNull() && less_or_cannot_compare(max_value, min_value)) + { + if (reaction == THROW_ON_VIOLATION) + throw Exception("Cannot modify '" + setting_name + "' setting in readonly mode", ErrorCodes::READONLY); + else + return false; + } + + // Check left bound + if (!min_value.isNull() && less_or_cannot_compare(new_value, min_value)) + { + if (reaction == THROW_ON_VIOLATION) + { + throw Exception( + "Setting " + setting_name + " in readonly mode shouldn't be less than " + applyVisitor(FieldVisitorToString(), constraint->min_value_in_readonly), + ErrorCodes::READONLY); + } + else + { + new_value = min_value; // to ensure `min` clamping will not cancel `min_in_readonly` clamp + change.value = min_value; + } + } + + // Check right bound + if (!max_value.isNull() && less_or_cannot_compare(max_value, new_value)) + { + if (reaction == THROW_ON_VIOLATION) + { + throw Exception( + "Setting " + setting_name + " in readonly mode shouldn't be greater than " + applyVisitor(FieldVisitorToString(), constraint->max_value_in_readonly), + ErrorCodes::READONLY); + } + else + { + new_value = max_value; // to ensure `max` clamping will not cancel `max_in_readonly` clamp + change.value = max_value; + } + } + } + if (constraint) { if (constraint->read_only) @@ -281,7 +361,7 @@ bool SettingsConstraints::checkImpl(const Settings & current_settings, SettingCh const Field & min_value = constraint->min_value; const Field & max_value = constraint->max_value; - if (!min_value.isNull() && !max_value.isNull() && (less(max_value, min_value) || cannot_compare)) + if (!min_value.isNull() && !max_value.isNull() && less_or_cannot_compare(max_value, min_value)) { if (reaction == THROW_ON_VIOLATION) throw Exception("Setting " + setting_name + " should not be changed", ErrorCodes::SETTING_CONSTRAINT_VIOLATION); @@ -289,7 +369,7 @@ bool SettingsConstraints::checkImpl(const Settings & current_settings, SettingCh return false; } - if (!min_value.isNull() && (less(new_value, min_value) || cannot_compare)) + if (!min_value.isNull() && less_or_cannot_compare(new_value, min_value)) { if (reaction == THROW_ON_VIOLATION) { @@ -301,7 +381,7 @@ bool SettingsConstraints::checkImpl(const Settings & current_settings, SettingCh change.value = min_value; } - if (!max_value.isNull() && (less(max_value, new_value) || cannot_compare)) + if (!max_value.isNull() && less_or_cannot_compare(max_value, new_value)) { if (reaction == THROW_ON_VIOLATION) { @@ -342,7 +422,9 @@ const SettingsConstraints::Constraint * SettingsConstraints::tryGetConstraint(st bool SettingsConstraints::Constraint::operator==(const Constraint & other) const { - return (read_only == other.read_only) && (min_value == other.min_value) && (max_value == other.max_value) + return (read_only == other.read_only) + && (min_value == other.min_value) && (max_value == other.max_value) + && (min_value_in_readonly == other.min_value_in_readonly) && (max_value_in_readonly == other.max_value_in_readonly) && (*setting_name == *other.setting_name); } diff --git a/src/Access/SettingsConstraints.h b/src/Access/SettingsConstraints.h index 645a690e051..5566578b101 100644 --- a/src/Access/SettingsConstraints.h +++ b/src/Access/SettingsConstraints.h @@ -33,6 +33,7 @@ class AccessControl; * * 200000 * 20000000000 + * 10000000000 * * * @@ -43,10 +44,11 @@ class AccessControl; * * This class also checks that we are not in the read-only mode. * If a setting cannot be change due to the read-only mode this class throws an exception. - * The value of `readonly` value is understood as follows: - * 0 - everything allowed. - * 1 - only read queries can be made; you can not change the settings. - * 2 - you can only do read queries and you can change the settings, except for the `readonly` setting. + * The value of `readonly` is understood as follows: + * 0 - not read-only mode, no additional checks. + * 1 - only read queries, as well as changing explicitly allowed settings. + * 2 - only read queries and you can change the settings, except for the `readonly` setting. + * */ class SettingsConstraints { @@ -70,8 +72,14 @@ public: void setReadOnly(std::string_view setting_name, bool read_only); bool isReadOnly(std::string_view setting_name) const; + void setMinValueInReadOnly(std::string_view setting_name, const Field & min_value_in_readonly); + Field getMinValueInReadOnly(std::string_view setting_name) const; + + void setMaxValueInReadOnly(std::string_view setting_name, const Field & max_value_in_readonly); + Field getMaxValueInReadOnly(std::string_view setting_name) const; + void set(std::string_view setting_name, const Field & min_value, const Field & max_value, bool read_only); - void get(std::string_view setting_name, Field & min_value, Field & max_value, bool & read_only) const; + void get(std::string_view setting_name, Field & min_value, Field & max_value, bool & read_only, Field & min_value_in_readonly, Field & max_value_in_readonly) const; void merge(const SettingsConstraints & other); @@ -94,6 +102,11 @@ private: Field min_value; Field max_value; + // Allowed range in `readonly=1` mode. + // NOTE: only reasonable to be a subset of [min_value; max_value] range, although there is no enforcement. + Field min_value_in_readonly; + Field max_value_in_readonly; + bool operator ==(const Constraint & other) const; bool operator !=(const Constraint & other) const { return !(*this == other); } }; diff --git a/src/Access/SettingsProfileElement.cpp b/src/Access/SettingsProfileElement.cpp index 465f26f37d9..aff41e3553e 100644 --- a/src/Access/SettingsProfileElement.cpp +++ b/src/Access/SettingsProfileElement.cpp @@ -214,6 +214,10 @@ SettingsConstraints SettingsProfileElements::toSettingsConstraints(const AccessC res.setMaxValue(elem.setting_name, elem.max_value); if (elem.readonly) res.setReadOnly(elem.setting_name, *elem.readonly); + if (!elem.min_value_in_readonly.isNull()) + res.setMinValueInReadOnly(elem.setting_name, elem.min_value_in_readonly); + if (!elem.max_value_in_readonly.isNull()) + res.setMaxValueInReadOnly(elem.setting_name, elem.max_value_in_readonly); } } return res; diff --git a/src/Access/SettingsProfileElement.h b/src/Access/SettingsProfileElement.h index a4124826b40..1f39cd1c788 100644 --- a/src/Access/SettingsProfileElement.h +++ b/src/Access/SettingsProfileElement.h @@ -26,8 +26,10 @@ struct SettingsProfileElement Field min_value; Field max_value; std::optional readonly; + Field min_value_in_readonly; + Field max_value_in_readonly; - auto toTuple() const { return std::tie(parent_profile, setting_name, value, min_value, max_value, readonly); } + auto toTuple() const { return std::tie(parent_profile, setting_name, value, min_value, max_value, readonly, min_value_in_readonly, max_value_in_readonly); } friend bool operator==(const SettingsProfileElement & lhs, const SettingsProfileElement & rhs) { return lhs.toTuple() == rhs.toTuple(); } friend bool operator!=(const SettingsProfileElement & lhs, const SettingsProfileElement & rhs) { return !(lhs == rhs); } friend bool operator <(const SettingsProfileElement & lhs, const SettingsProfileElement & rhs) { return lhs.toTuple() < rhs.toTuple(); } diff --git a/src/Access/UsersConfigAccessStorage.cpp b/src/Access/UsersConfigAccessStorage.cpp index 1d755fdf1da..ceacdcdb546 100644 --- a/src/Access/UsersConfigAccessStorage.cpp +++ b/src/Access/UsersConfigAccessStorage.cpp @@ -449,6 +449,10 @@ namespace profile_element.max_value = Settings::stringToValueUtil(setting_name, config.getString(path_to_name + "." + constraint_type)); else if (constraint_type == "readonly") profile_element.readonly = true; + else if (constraint_type == "min_in_readonly") + profile_element.min_value_in_readonly = Settings::stringToValueUtil(setting_name, config.getString(path_to_name + "." + constraint_type)); + else if (constraint_type == "max_in_readonly") + profile_element.max_value_in_readonly = Settings::stringToValueUtil(setting_name, config.getString(path_to_name + "." + constraint_type)); else throw Exception("Setting " + constraint_type + " value for " + setting_name + " isn't supported", ErrorCodes::NOT_IMPLEMENTED); } diff --git a/src/Core/Settings.h b/src/Core/Settings.h index 2b808a1ada7..6ab6ad8bb5c 100644 --- a/src/Core/Settings.h +++ b/src/Core/Settings.h @@ -350,7 +350,7 @@ static constexpr UInt64 operator""_GiB(unsigned long long value) M(UInt64, max_ast_elements, 50000, "Maximum size of query syntax tree in number of nodes. Checked after parsing.", 0) \ M(UInt64, max_expanded_ast_elements, 500000, "Maximum size of query syntax tree in number of nodes after expansion of aliases and the asterisk.", 0) \ \ - M(UInt64, readonly, 0, "0 - everything is allowed. 1 - only read requests. 2 - only read requests, as well as changing settings, except for the 'readonly' setting.", 0) \ + M(UInt64, readonly, 0, "0 - no read-only restrictions. 1 - only read requests, as well as changing explicitly allowed settings. 2 - only read requests, as well as changing settings, except for the 'readonly' setting.", 0) \ \ M(UInt64, max_rows_in_set, 0, "Maximum size of the set (in number of elements) resulting from the execution of the IN section.", 0) \ M(UInt64, max_bytes_in_set, 0, "Maximum size of the set (in bytes in memory) resulting from the execution of the IN section.", 0) \ diff --git a/src/Storages/System/StorageSystemSettings.cpp b/src/Storages/System/StorageSystemSettings.cpp index e1f1e4985b4..cac90e93bb6 100644 --- a/src/Storages/System/StorageSystemSettings.cpp +++ b/src/Storages/System/StorageSystemSettings.cpp @@ -18,6 +18,8 @@ NamesAndTypesList StorageSystemSettings::getNamesAndTypes() {"min", std::make_shared(std::make_shared())}, {"max", std::make_shared(std::make_shared())}, {"readonly", std::make_shared()}, + {"min_in_readonly", std::make_shared(std::make_shared())}, + {"max_in_readonly", std::make_shared(std::make_shared())}, {"type", std::make_shared()}, }; } @@ -40,8 +42,9 @@ void StorageSystemSettings::fillData(MutableColumns & res_columns, ContextPtr co res_columns[3]->insert(setting.getDescription()); Field min, max; + Field min_in_readonly, max_in_readonly; bool read_only = false; - constraints.get(setting_name, min, max, read_only); + constraints.get(setting_name, min, max, read_only, min_in_readonly, max_in_readonly); /// These two columns can accept strings only. if (!min.isNull()) @@ -57,10 +60,18 @@ void StorageSystemSettings::fillData(MutableColumns & res_columns, ContextPtr co read_only = true; } + /// These two columns can accept strings only. + if (!min_in_readonly.isNull()) + min_in_readonly = Settings::valueToStringUtil(setting_name, min_in_readonly); + if (!max_in_readonly.isNull()) + max_in_readonly = Settings::valueToStringUtil(setting_name, max_in_readonly); + res_columns[4]->insert(min); res_columns[5]->insert(max); res_columns[6]->insert(read_only); - res_columns[7]->insert(setting.getTypeName()); + res_columns[7]->insert(min_in_readonly); + res_columns[8]->insert(max_in_readonly); + res_columns[9]->insert(setting.getTypeName()); } } diff --git a/src/Storages/System/StorageSystemSettingsProfileElements.cpp b/src/Storages/System/StorageSystemSettingsProfileElements.cpp index 565ff5e471e..7dcec16204e 100644 --- a/src/Storages/System/StorageSystemSettingsProfileElements.cpp +++ b/src/Storages/System/StorageSystemSettingsProfileElements.cpp @@ -29,6 +29,8 @@ NamesAndTypesList StorageSystemSettingsProfileElements::getNamesAndTypes() {"min", std::make_shared(std::make_shared())}, {"max", std::make_shared(std::make_shared())}, {"readonly", std::make_shared(std::make_shared())}, + {"min_in_readonly", std::make_shared(std::make_shared())}, + {"max_in_readonly", std::make_shared(std::make_shared())}, {"inherit_profile", std::make_shared(std::make_shared())}, }; return names_and_types; @@ -64,6 +66,10 @@ void StorageSystemSettingsProfileElements::fillData(MutableColumns & res_columns auto & column_max_null_map = assert_cast(*res_columns[i++]).getNullMapData(); auto & column_readonly = assert_cast(assert_cast(*res_columns[i]).getNestedColumn()).getData(); auto & column_readonly_null_map = assert_cast(*res_columns[i++]).getNullMapData(); + auto & column_min_in_readonly = assert_cast(assert_cast(*res_columns[i]).getNestedColumn()); + auto & column_min_in_readonly_null_map = assert_cast(*res_columns[i++]).getNullMapData(); + auto & column_max_in_readonly = assert_cast(assert_cast(*res_columns[i]).getNestedColumn()); + auto & column_max_in_readonly_null_map = assert_cast(*res_columns[i++]).getNullMapData(); auto & column_inherit_profile = assert_cast(assert_cast(*res_columns[i]).getNestedColumn()); auto & column_inherit_profile_null_map = assert_cast(*res_columns[i++]).getNullMapData(); @@ -108,8 +114,26 @@ void StorageSystemSettingsProfileElements::fillData(MutableColumns & res_columns inserted_readonly = true; } + bool inserted_min_in_readonly = false; + if (!element.min_value_in_readonly.isNull() && !element.setting_name.empty()) + { + String str = Settings::valueToStringUtil(element.setting_name, element.min_value_in_readonly); + column_min_in_readonly.insertData(str.data(), str.length()); + column_min_in_readonly_null_map.push_back(false); + inserted_min_in_readonly = true; + } + + bool inserted_max_in_readonly = false; + if (!element.max_value_in_readonly.isNull() && !element.setting_name.empty()) + { + String str = Settings::valueToStringUtil(element.setting_name, element.max_value_in_readonly); + column_max_in_readonly.insertData(str.data(), str.length()); + column_max_in_readonly_null_map.push_back(false); + inserted_max_in_readonly = true; + } + bool inserted_setting_name = false; - if (inserted_value || inserted_min || inserted_max || inserted_readonly) + if (inserted_value || inserted_min || inserted_max || inserted_readonly || inserted_min_in_readonly || inserted_max_in_readonly) { const auto & setting_name = element.setting_name; column_setting_name.insertData(setting_name.data(), setting_name.size());