Allow to modify constrained settings in readonly mode

This commit is contained in:
Sergei Trifonov 2022-08-25 17:24:24 +02:00
parent c5c6f8a3fe
commit 856a2f5956
10 changed files with 198 additions and 33 deletions

View File

@ -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 isnt 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
<profiles>
<user_name>
<constraints>
<setting_name_1>
<min_in_readonly>lower_boundary</min_in_readonly>
</setting_name_1>
<setting_name_2>
<max_in_readonly>upper_boundary</max_in_readonly>
</setting_name_2>
<setting_name_3>
<min_in_readonly>lower_boundary</min_in_readonly>
<max_in_readonly>upper_boundary</max_in_readonly>
</setting_name_3>
</constraints>
</user_name>
</profiles>
```
**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 theyre overridden explicitly for these users.
[Original article](https://clickhouse.com/docs/en/operations/settings/constraints_on_settings/) <!--hide-->

View File

@ -36,8 +36,7 @@ After setting `readonly = 1`, the user cant 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

View File

@ -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);
}

View File

@ -33,6 +33,7 @@ class AccessControl;
* <max_memory_usage>
* <min>200000</min>
* <max>20000000000</max>
* <max_in_readonly>10000000000</max_in_readonly>
* </max_memory_usage>
* <force_index_by_date>
* <readonly/>
@ -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); }
};

View File

@ -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;

View File

@ -26,8 +26,10 @@ struct SettingsProfileElement
Field min_value;
Field max_value;
std::optional<bool> 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(); }

View File

@ -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);
}

View File

@ -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) \

View File

@ -18,6 +18,8 @@ NamesAndTypesList StorageSystemSettings::getNamesAndTypes()
{"min", std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>())},
{"max", std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>())},
{"readonly", std::make_shared<DataTypeUInt8>()},
{"min_in_readonly", std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>())},
{"max_in_readonly", std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>())},
{"type", std::make_shared<DataTypeString>()},
};
}
@ -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());
}
}

View File

@ -29,6 +29,8 @@ NamesAndTypesList StorageSystemSettingsProfileElements::getNamesAndTypes()
{"min", std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>())},
{"max", std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>())},
{"readonly", std::make_shared<DataTypeNullable>(std::make_shared<DataTypeUInt8>())},
{"min_in_readonly", std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>())},
{"max_in_readonly", std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>())},
{"inherit_profile", std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>())},
};
return names_and_types;
@ -64,6 +66,10 @@ void StorageSystemSettingsProfileElements::fillData(MutableColumns & res_columns
auto & column_max_null_map = assert_cast<ColumnNullable &>(*res_columns[i++]).getNullMapData();
auto & column_readonly = assert_cast<ColumnUInt8 &>(assert_cast<ColumnNullable &>(*res_columns[i]).getNestedColumn()).getData();
auto & column_readonly_null_map = assert_cast<ColumnNullable &>(*res_columns[i++]).getNullMapData();
auto & column_min_in_readonly = assert_cast<ColumnString &>(assert_cast<ColumnNullable &>(*res_columns[i]).getNestedColumn());
auto & column_min_in_readonly_null_map = assert_cast<ColumnNullable &>(*res_columns[i++]).getNullMapData();
auto & column_max_in_readonly = assert_cast<ColumnString &>(assert_cast<ColumnNullable &>(*res_columns[i]).getNestedColumn());
auto & column_max_in_readonly_null_map = assert_cast<ColumnNullable &>(*res_columns[i++]).getNullMapData();
auto & column_inherit_profile = assert_cast<ColumnString &>(assert_cast<ColumnNullable &>(*res_columns[i]).getNestedColumn());
auto & column_inherit_profile_null_map = assert_cast<ColumnNullable &>(*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());